├── README.md ├── .gitignore ├── listener ├── TestLinkListener.py ├── TagLabel.py ├── TestRailListener.py └── LogGrabber.py ├── library ├── WinRMLibrary.py ├── ZookeeperManager.py ├── AdvancedLogging.py ├── TestlinkAPIClient.py ├── OracleDB.py ├── JsonValidator.py └── RabbitMqManager.py ├── LICENSE └── doc └── AdvancedLogging.html /README.md: -------------------------------------------------------------------------------- 1 | **The libraries of this repository are moved to https://github.com/topics/robotframework?utf8=%E2%9C%93&q=org%3Apeterservice-rnd&type=** 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | .idea/ 37 | -------------------------------------------------------------------------------- /listener/TestLinkListener.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from robot.api import logger 4 | from TestlinkAPIClient import TestlinkAPIClient as testlink 5 | 6 | class TestLinkListener(object): 7 | """ 8 | Фиксирование результатов тестирования в TestLink.\n 9 | 10 | """ 11 | 12 | ROBOT_LISTENER_API_VERSION = 2 13 | 14 | def __init__(self, testlinkuri, devKey, testplanID): 15 | self.server = 'http://'+testlinkuri+'/testlink/lib/api/xmlrpc.php' 16 | self.devKey = devKey 17 | self.testplanID = testplanID 18 | self.tl = testlink(self.server,self.devKey,self.testplanID) 19 | self.buildID = self.tl.getLatestBuildForTestPlan() 20 | logger.info('[TestLinkListener] server url: ' + self.server) 21 | logger.info('[TestLinkListener] developer key: ' + self.devKey) 22 | logger.info('[TestLinkListener] testplanID: ' + self.testplanID) 23 | logger.info('[TestLinkListener] buildID: ' + self.buildID) 24 | 25 | def end_test(self, name, attrs): 26 | """ 27 | Сохранение результата выполнения теста в TestLink 28 | 29 | Входные параметры:\n 30 | name - имя тест-кейса\n 31 | attrs - атрибуты теста 32 | """ 33 | testcaseID = self.tl.getTestCaseIDFromTestTags (attrs['tags']) 34 | if attrs['status'] == 'PASS': 35 | status = 'p' 36 | else : 37 | status = 'f' 38 | if testcaseID: 39 | self.tl.reportTCResult (testcaseID,self.buildID,status,attrs['message']) 40 | logger.info ('[TestLinkListener] report result of test ' + testcaseID + ' to TestLink') 41 | -------------------------------------------------------------------------------- /library/WinRMLibrary.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from robot.api import logger 4 | from robot.utils import ConnectionCache 5 | import winrm 6 | 7 | class WinRMLibrary(object): 8 | """ 9 | Robot Framework library for Windows Remote Management, based on pywinrm. 10 | 11 | == Enable Windows Remote Shell == 12 | - [ http://support.microsoft.com/kb/555966 | KB-555966 ] 13 | - Execute on windows server: 14 | 15 | | winrm set winrm/config/client/auth @{Basic="true"} 16 | | winrm set winrm/config/service/auth @{Basic="true"} 17 | | winrm set winrm/config/service @{AllowUnencrypted="true"} 18 | 19 | == Dependence == 20 | | pywinrm | https://pypi.python.org/pypi/pywinrm | 21 | | robot framework | http://robotframework.org | 22 | """ 23 | 24 | ROBOT_LIBRARY_SCOPE='GLOBAL' 25 | 26 | def __init__(self): 27 | self._session=None 28 | self._cache=ConnectionCache('No sessions created') 29 | 30 | def create_session (self, alias, hostname, login, password): 31 | """ 32 | Create session with windows host. 33 | 34 | Does not support domain authentification. 35 | 36 | *Args:*\n 37 | _alias_ - robot framework alias to identify the session\n 38 | _hostname_ - windows hostname (not IP)\n 39 | _login_ - windows local login\n 40 | _password_ - windows local password 41 | 42 | *Returns:*\n 43 | Session index 44 | 45 | *Example:*\n 46 | | Create Session | server | windows-host | Administrator | 1234567890 | 47 | """ 48 | 49 | logger.debug ('Connecting using : hostname=%s, login=%s, password=%s '%(hostname, login, password)) 50 | self._session=winrm.Session(hostname, (login, password)) 51 | return self._cache.register(self._session, alias) 52 | 53 | def run_cmd (self, alias, command, params=None): 54 | """ 55 | Execute command on remote mashine. 56 | 57 | *Args:*\n 58 | _alias_ - robot framework alias to identify the session\n 59 | _command_ - windows command\n 60 | _params_ - lists of command's parameters 61 | 62 | *Returns:*\n 63 | Result object with methods: status_code, std_out, std_err. 64 | 65 | *Example:*\n 66 | | ${params}= | Create List | "/all" | 67 | | ${result}= | Run cmd | server | ipconfig | ${params} | 68 | | Log | ${result.status_code} | 69 | | Log | ${result.std_out} | 70 | | Log | ${result.std_err} | 71 | =>\n 72 | | 0 73 | | Windows IP Configuration 74 | | Host Name . . . . . . . . . . . . : WINDOWS-HOST 75 | | Primary Dns Suffix . . . . . . . : 76 | | Node Type . . . . . . . . . . . . : Hybrid 77 | | IP Routing Enabled. . . . . . . . : No 78 | | WINS Proxy Enabled. . . . . . . . : No 79 | | 80 | """ 81 | 82 | if params is not None: 83 | log_cmd=command+' '+' '.join(params) 84 | else: 85 | log_cmd=command 86 | logger.info ('Run command on server with alias "%s": %s '%(alias, log_cmd)) 87 | self._session=self._cache.switch(alias) 88 | result=self._session.run_cmd (command, params) 89 | return result 90 | 91 | def run_ps (self, alias, script): 92 | """ 93 | Run power shell script on remote mashine. 94 | 95 | *Args:*\n 96 | _alias_ - robot framework alias to identify the session\n 97 | _script_ - power shell script\n 98 | 99 | *Returns:*\n 100 | Result object with methods: status_code, std_out, std_err. 101 | 102 | *Example:*\n 103 | 104 | | ${result}= | Run ps | server | get-process iexplore|select -exp ws|measure-object -sum|select -exp Sum | 105 | | Log | ${result.status_code} | 106 | | Log | ${result.std_out} | 107 | | Log | ${result.std_err} | 108 | =>\n 109 | | 0 110 | | 56987648 111 | | 112 | """ 113 | 114 | logger.info ('Run power shell script on server with alias "%s": %s '%(alias, script)) 115 | self._session=self._cache.switch(alias) 116 | result=self._session.run_ps (script) 117 | return result 118 | 119 | def delete_all_sessions(self): 120 | """ Removes all sessions with windows hosts""" 121 | 122 | self._cache.empty_cache() -------------------------------------------------------------------------------- /listener/TagLabel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import re 6 | import logging 7 | from robot.api import TestData 8 | from robot.errors import DataError 9 | from robot.writer.datafilewriter import DataFileWriter 10 | 11 | from testrail import APIClient 12 | 13 | TEST_RAIL_ID_TEMPLATE = 'testRailId={case_id}' 14 | # todo: add other tags to existing test cases 15 | TAG_TYPE = { 16 | 'testRail': 'add_test_rail_id', 17 | 'defects': 'add_defects', 18 | 'references': 'add_references' 19 | } 20 | 21 | 22 | def getTagsValue(tags): 23 | """ 24 | Get value from robot framework's tags for TestRail. 25 | """ 26 | attributes = dict() 27 | matchers = ['testRailId', 'defects', 'references'] 28 | for matcher in matchers: 29 | for tag in tags: 30 | match = re.match(matcher, tag) 31 | if match: 32 | split_tag = tag.split('=') 33 | tag_value = split_tag[1] 34 | attributes[matcher] = tag_value 35 | break 36 | else: 37 | attributes[matcher] = None 38 | return attributes 39 | 40 | 41 | class TestRailTagger(object): 42 | """ 43 | class to register robot's test cases in test rail 44 | and then add corresponding tags to robot's test cases 45 | """ 46 | 47 | def __init__(self, host, user, password, 48 | file_path, section_id, 49 | file_format='robot'): 50 | """ 51 | :param host: test rail host 52 | :param user: user name 53 | :param password: password 54 | :param file_path: path of test case files or directory 55 | :param section_id: section to store auto test cases 56 | :param file_format: default to be .robot 57 | """ 58 | testrail_url = 'http://' + host + '/testrail/' 59 | self.client = APIClient(testrail_url, user, password) 60 | self.file = list() 61 | # to loop through the test suites 62 | try: 63 | if os.path.isdir(file_path): 64 | for files in os.walk(file_path): 65 | for robot_file in filter(lambda x: x.endswith('.robot'), files[2]): 66 | self.file.append( 67 | TestData( 68 | source=os.path.abspath(os.path.join(files[0], 69 | robot_file)) 70 | ) 71 | ) 72 | else: 73 | self.file.append(TestData(source=file_path)) 74 | except DataError as e: 75 | # .robot file may have no test cases in it 76 | logging.warn('[TestRailTagger]' + e.message) 77 | 78 | self.section = section_id 79 | self.writer = DataFileWriter(format=file_format) 80 | 81 | def add_tag(self): 82 | """ 83 | add specific tags to test cases 84 | """ 85 | for suite in self.file: 86 | # to handle force tags, delete force tags in setting table 87 | # and then add the tag value to each test case in a suite 88 | force_tags = suite.setting_table.force_tags.value 89 | suite.setting_table.force_tags.value = None 90 | for test in suite.testcase_table.tests: 91 | getattr(self, TAG_TYPE['testRail'])(test, suite, force_tags) 92 | 93 | def add_test_rail_id(self, test, test_case_file, other_tags): 94 | """ 95 | register test case and add the test case id to .robot file 96 | :param test: TestData class object, one of test cases 97 | read from goven .robot file 98 | :param test_case_file: the .robot file to write (add tags) 99 | :param other_tags: param for the force tags(also for other tags) 100 | """ 101 | if test.tags.value is None: 102 | test.tags.value = list() 103 | tags_value = getTagsValue(test.tags.value) 104 | case_id = tags_value.get('testRailId') 105 | if case_id is None: 106 | res = self.client.send_post( 107 | 'add_case/{section_id}'.format(section_id=self.section), 108 | { 109 | 'title': 'Auto-' + test.name, 110 | 'type_id': 1, 111 | 'priority_id': 3, 112 | 'estimate': '1h', 113 | 'refs': '', 114 | 'custom_steps': [ 115 | { 116 | 'content': getattr(step, 'name', ''), 117 | 'expected': 'auto-results' 118 | } for step in test.steps if step 119 | ] 120 | } 121 | ) 122 | case_id = res.get('id') 123 | logging.info('[TestRailTagger] register test {case_id} to TestRail' 124 | .format(case_id=case_id)) 125 | test.tags.value.append(TEST_RAIL_ID_TEMPLATE.format(case_id=case_id)) 126 | test.tags.value += other_tags 127 | self.writer.write(test_case_file) 128 | -------------------------------------------------------------------------------- /library/ZookeeperManager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from robot.libraries.BuiltIn import BuiltIn 3 | from kazoo.client import KazooClient 4 | from kazoo.exceptions import NoNodeError 5 | 6 | class ZookeeperManager(object): 7 | """ 8 | Working with Apache Zookeeper. 9 | Based on Kazoo (Python library). All errors described in "Returns" section are kazoo exceptions (kazoo.exceptions). 10 | 11 | == Notice == 12 | kazoo library work only on Linux now 13 | 14 | == Dependence == 15 | | robot framework | http://robotframework.org | 16 | | kazoo | https://github.com/python-zk/kazoo | 17 | """ 18 | 19 | ROBOT_LIBRARY_SCOPE='GLOBAL' 20 | 21 | def __init__(self): 22 | self.bi = BuiltIn() 23 | 24 | def connect_to_zookeeper(self, hosts, timeout=10): 25 | """ 26 | Connecting to Zookeeper 27 | 28 | *Args:*\n 29 | _host_ - comma-separated list of hosts to connect.\n 30 | _timeout_ - connection timeout in seconds. Default timeout is 10 seconds.\n 31 | 32 | *Example:*\n 33 | | Connect To Zookeeper | host1:2181, host2 | 25 | 34 | """ 35 | self.zk = KazooClient(hosts, timeout) 36 | self.zk.start() 37 | 38 | def disconnect_from_zookeeper(self): 39 | """ 40 | Close all connections to Zookeeper 41 | 42 | *Example:*\n 43 | | Connect To Zookeeper | 127.0.0.1: 2181 | 44 | | Disconnect From Zookeeper | 45 | """ 46 | self.zk.stop() 47 | self.zk.close() 48 | 49 | def create_node(self, path, value='', force=False): 50 | """ 51 | Create a node with a value. 52 | 53 | *Args:*\n 54 | _path_ - node path.\n 55 | _value_ - node value. Default is an empty string.\n 56 | _force_ - if TRUE parent path will be created. Default value is FALSE.\n 57 | 58 | *Raises:*\n 59 | _NodeExistError_ - node already exists.\n 60 | _NoNodeError_ - parent node doesn't exist.\n 61 | _NoChildrenForEphemeralsError_ - parent node is an ephemeral mode.\n 62 | _ZookeeperError_ - value is too large or server returns a non-zero error code.\n 63 | 64 | *Example:*\n 65 | | Create Node | /my/favorite/node | my_value | ${TRUE} | 66 | """ 67 | string_value = value.encode('utf-8') 68 | self.zk.create(path, string_value, None, False, False, force) 69 | 70 | def delete_node(self, path, force=False): 71 | """ 72 | Delete a node. 73 | 74 | *Args:*\n 75 | _path_ - node path.\n 76 | _force_ - if TRUE and node exists - recursively delete a node with all its children, 77 | if TRUE and node doesn't exists - error won't be raised. Default value is FALSE. 78 | 79 | *Raises:*\n 80 | _NoNodeError_ - node doesn't exist.\n 81 | _NotEmptyError_ - node has children.\n 82 | _ZookeeperError_ - server returns a non-zero error code. 83 | """ 84 | try: 85 | self.zk.delete(path, -1, force) 86 | except NoNodeError: 87 | if (force): 88 | pass 89 | else: 90 | raise 91 | 92 | def exists(self, path): 93 | """ 94 | Check is a node exists. 95 | 96 | *Args:*\n 97 | _path_ - node path. 98 | 99 | *Returns:*\n 100 | TRUE if a node exists, FALSE in other way. 101 | """ 102 | node_stat = self.zk.exists(path) 103 | if (node_stat != None): 104 | #Node exists 105 | return True 106 | else: 107 | #Node doesn't exist 108 | return False 109 | 110 | def set_value(self, path, value, force=False): 111 | """ 112 | Set the value of a node. 113 | 114 | *Args:*\n 115 | _path_ - node path.\n 116 | _value_ - new value.\n 117 | _force_ - if TRUE path will be created. Default value is FALSE.\n 118 | 119 | *Raises:*\n 120 | _NoNodeError - node doesn't exist.\n 121 | _ZookeeperError - value is too large or server returns non-zero error code. 122 | """ 123 | string_value = value.encode('utf-8') 124 | if (force): 125 | self.zk.ensure_path(path) 126 | self.zk.set(path, string_value) 127 | 128 | def get_value(self, path): 129 | """ 130 | Get the value of a node. 131 | 132 | *Args:*\n 133 | _path_ - node path. 134 | 135 | *Returns:*\n 136 | Value of a node. 137 | 138 | *Raises:*\n 139 | _NoNodeError_ - node doesn't exist.\n 140 | _ZookeeperError_ - server returns a non-zero error code. 141 | """ 142 | value, _stat = self.zk.get(path) 143 | return value 144 | 145 | def get_children(self, path): 146 | """ 147 | Get a list of node's children. 148 | 149 | *Args:*\n 150 | _path_ - node path. 151 | 152 | *Returns:*\n 153 | List of node's children. 154 | 155 | *Raises:*\n 156 | _NoNodeError_ - node doesn't exist.\n 157 | _ZookeeperError_ - server returns a non-zero error code. 158 | """ 159 | children = self.zk.get_children(path) 160 | return children -------------------------------------------------------------------------------- /library/AdvancedLogging.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from robot.libraries.BuiltIn import BuiltIn 4 | from robot.libraries.OperatingSystem import OperatingSystem 5 | from robot.running.context import EXECUTION_CONTEXTS 6 | import os 7 | 8 | 9 | class AdvancedLogging(object): 10 | """ 11 | Creating additional logs when testing. 12 | If during the test you want to add any additional information in the file, then this library 13 | provide a hierarchy of folders and files for logging. 14 | Folder hierarchy is created as follows: output_dir/test_log_folder_name/Test_Suite/Test_Suite/Test_Case/file.log 15 | Log files and folders are not removed before the test, and overwritten of new files. 16 | 17 | == Dependency: == 18 | | robot framework | http://robotframework.org | 19 | ------- 20 | 21 | When initializing the library, you can define two optional arguments 22 | | *Argument Name* | *Default value* | *Description* | 23 | | output_dir | ${OUTPUT_DIR} | The directory in which create the folder with additional logs | 24 | | test_log_folder_name | Advanced_Logs | Name of the folder from which to build a hierarchy of logs | 25 | ------- 26 | 27 | == Example: == 28 | | *Settings* | *Value* | *Value* | *Value* | 29 | | Library | AdvancedLogging | C:/Temp | LogFromServer | 30 | | Library | SSHLibrary | | | 31 | 32 | | *Test cases* | *Action* | *Argument* | *Argument* | 33 | | Example_TestCase | ${out}= | Execute Command | grep error output.log | 34 | | | Write advanced testlog | error.log | ${out} | 35 | =>\n 36 | File C:/Temp/LogFromServer/TestSuite name/Example_TestCase/error.log with content from variable ${out} 37 | """ 38 | 39 | ROBOT_LIBRARY_SCOPE = 'TEST SUITE' 40 | 41 | def __init__(self, output_dir=None, test_log_folder_name='Advanced_Logs'): 42 | """ Initialisation 43 | 44 | *Args*:\n 45 | _output_dir_: output directory.\n 46 | _test_log_folder_name_: name for log folder 47 | """ 48 | self.os = OperatingSystem() 49 | self.bi = BuiltIn() 50 | 51 | self.output_dir = output_dir 52 | self.test_log_folder_name = test_log_folder_name 53 | self.sep = '/' 54 | 55 | def _get_suite_names(self): 56 | """ 57 | Get List with the current suite name and all its parents names 58 | 59 | *Returns:*\n 60 | List of the current suite name and all its parents names 61 | """ 62 | suite = EXECUTION_CONTEXTS.current.suite 63 | result = [suite.name] 64 | while suite.parent: 65 | suite = suite.parent 66 | result.append(suite.name) 67 | return reversed(result) 68 | 69 | @property 70 | def _suite_folder(self): 71 | """ 72 | Define variables that are initialized by a call 'TestSuite' 73 | 74 | *Returns:*\n 75 | Path to suite folder. 76 | """ 77 | 78 | if self.output_dir is None: 79 | self.output_dir = self.bi.get_variable_value('${OUTPUT_DIR}') 80 | 81 | suite_name = self.sep.join(self._get_suite_names()) 82 | self.output_dir = os.path.normpath(self.output_dir) 83 | self.test_log_folder_name = os.path.normpath(self.test_log_folder_name) 84 | 85 | suite_folder = self.output_dir + self.sep + self.test_log_folder_name + self.sep + suite_name 86 | return os.path.normpath(suite_folder) 87 | 88 | def write_advanced_testlog(self, filename, content): 89 | """ 90 | Inclusion content in additional log file 91 | 92 | *Args:*\n 93 | _filename_ - name of log file 94 | _content_- content for logging 95 | 96 | *Returns:*\n 97 | Path to filename. 98 | 99 | *Example:*\n 100 | | Write advanced testlog | log_for_test.log | test message | 101 | =>\n 102 | File ${OUTPUT_DIR}/Advanced_Logs///log_for_test.log with content 'test message' 103 | """ 104 | 105 | test_name = BuiltIn().get_variable_value('${TEST_NAME}') 106 | 107 | if not test_name: 108 | log_folder = self._suite_folder + self.sep 109 | else: 110 | log_folder = self._suite_folder + self.sep + test_name 111 | 112 | self.os.create_file(log_folder + self.sep + filename, content) 113 | 114 | return os.path.normpath(log_folder + self.sep + filename) 115 | 116 | def create_advanced_logdir(self): 117 | """ 118 | Creating a folder hierarchy for TestSuite 119 | 120 | *Returns:*\n 121 | Path to folder. 122 | 123 | *Example:*\n 124 | | *Settings* | *Value* | 125 | | Library | AdvancedLogging | 126 | | Library | OperatingSystem | 127 | 128 | | *Test Cases* | *Action* | *Argument* | 129 | | ${ADV_LOGS_DIR}= | Create advanced logdir | | 130 | | Create file | ${ADV_LOGS_DIR}/log_for_suite.log | test message | 131 | =>\n 132 | File ${OUTPUT_DIR}/Advanced_Logs//log_for_suite.log with content 'test message' 133 | """ 134 | 135 | test_name = self.bi.get_variable_value('${TEST_NAME}') 136 | 137 | if not test_name: 138 | log_folder = self._suite_folder + self.sep 139 | else: 140 | log_folder = self._suite_folder + self.sep+test_name 141 | 142 | self.os.create_directory(os.path.normpath(log_folder)) 143 | 144 | return os.path.normpath(log_folder) 145 | -------------------------------------------------------------------------------- /listener/TestRailListener.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | import json 5 | from robot.api import logger 6 | from testrail import APIClient 7 | from robot.libraries.BuiltIn import BuiltIn 8 | # from idlelib.rpc import _getattributes 9 | 10 | __author__ = "Dmitriy.Zverev" 11 | __license__ = "Apache License, Version 2.0" 12 | 13 | class TestRailListener(object): 14 | """ 15 | Fixing of testing results and update test case in [ http://www.gurock.com/testrail/ | TestRail ].\n 16 | == Dependency == 17 | - [ http://docs.gurock.com/testrail-api2/bindings-python | TestRail API2 python binding] 18 | == Preconditions == 19 | 1. [ http://docs.gurock.com/testrail-api2/introduction | Enable TestRail API] \n 20 | 2. Create custom field "case_description" with type "text", which corresponds to the Robot Framework's test case documentation. 21 | == Example == 22 | 1. Create test case in TestRail with case_id = 10\n 23 | 2. Add it to test run with id run_id = 20\n 24 | 3. Create autotest in Robot Framework 25 | | *** Settings *** 26 | | *** Test Cases *** 27 | | Autotest name 28 | | [Documentation] Autotest documentation 29 | | [Tags] testrailid=10 defects=BUG-1, BUG-2 references=REF-3, REF-4 30 | | Fail Test fail message 31 | 4. Run Robot Framework with listener:\n 32 | | set ROBOT_SYSLOG_FILE=syslog.txt 33 | | pybot.bat --listener TestRailListener.py:testrail_server_name:tester_user_name:tester_user_password:20:update autotest.txt 34 | 5. Test with case_id=10 will be marked as failed in TestRail with message "Test fail message" and defects "BUG-1, BUG-2". 35 | Also title, description and references of this test will be updated in TestRail. Parameter "update" is optional. 36 | """ 37 | 38 | ROBOT_LISTENER_API_VERSION = 3 39 | 40 | def __init__(self, server, user, password, run_id, update = None): 41 | """ 42 | *Args:*\n 43 | _server_ - the name of TestRail server; 44 | _user_ - the name of TestRail user; 45 | _password_ - the password of TestRail user; 46 | _run_id - the ID of the test run; 47 | _update_ - indicator to update test case in TestRail; if exist, then test will be updated. 48 | """ 49 | 50 | testrail_url = 'http://' + server + '/testrail/' 51 | self.client = APIClient(testrail_url) 52 | self.client.user = user 53 | self.client.password = password 54 | self.run_id = run_id 55 | self.update = update 56 | self.results = list() 57 | logger.info('[TestRailListener] url: ' + testrail_url) 58 | logger.info('[TestRailListener] user: ' + user) 59 | logger.info('[TestRailListener] password: ' + password) 60 | logger.info('[TestRailListener] the ID of the test run: ' + run_id) 61 | 62 | def end_test(self, name, attrs): 63 | """ 64 | Update test case in TestRail 65 | 66 | *Args:*\n 67 | _name_ - the name of test case in Robot Framework\n 68 | _attrs_ - attributes of test case in Robot Framework 69 | """ 70 | 71 | tags_value = self._getTagsValue (attrs['tags']) 72 | case_id = tags_value['testrailid'] 73 | defects = tags_value['defects'] 74 | references = tags_value['references'] 75 | if attrs['status'] == 'PASS': 76 | status_id = 1 77 | else : 78 | status_id = 5 79 | if case_id: 80 | # Add results to list 81 | test_result = { 82 | 'case_id': case_id, 83 | 'status_id': status_id, 84 | 'comment': attrs['message'], 85 | 'defects': defects 86 | } 87 | self.results.append(test_result) 88 | 89 | # Update test case 90 | if self.update: 91 | logger.info ('[TestRailListener] update of test ' + case_id + ' in TestRail') 92 | description = attrs['doc'] + '\n' + 'Path to test: ' + attrs['longname'] 93 | result = self.client.send_post( 94 | 'update_case/' + case_id, 95 | { 96 | 'title': name, 97 | 'type_id': 1, 98 | 'custom_case_description': description, 99 | 'refs': references 100 | } 101 | ) 102 | logger.info ( 103 | '[TestRailListener] result for method update_case ' 104 | + json.dumps(result, sort_keys=True, indent=4) 105 | ) 106 | tag = 'testRailId={case_id}'.format(case_id=case_id) 107 | logger.info('[TestRailListener] remove tag {tag} from test case report'.format(tag=tag)) 108 | BuiltIn().run_keyword('remove tags', tag) 109 | 110 | def close (self): 111 | """ 112 | Save test results for all tests in TestRail 113 | """ 114 | 115 | self.client.send_post('add_results_for_cases/' + self.run_id, {'results': self.results}) 116 | 117 | def _getTagsValue (self, tags): 118 | """ 119 | Get value from robot framework's tags for TestRail. 120 | """ 121 | attributes = dict() 122 | matchers = ['testrailid', 'defects', 'references'] 123 | for matcher in matchers: 124 | for tag in tags: 125 | match = re.match(matcher, tag) 126 | if match: 127 | split_tag = tag.split('=') 128 | tag_value = split_tag[1] 129 | attributes[matcher] = tag_value 130 | break 131 | else: 132 | attributes[matcher] = None 133 | return attributes 134 | -------------------------------------------------------------------------------- /library/TestlinkAPIClient.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import xmlrpclib 3 | import re 4 | from robot.api import logger 5 | 6 | class TestlinkAPIClient(object): 7 | """ 8 | Реализация REST XML-API взаимодействия с [ http://testlink.org/ | TestLink ]. 9 | 10 | Реализована на основе: 11 | - [ http://jetmore.org/john/misc/phpdoc-testlink193-api/TestlinkAPI/TestlinkXMLRPCServer.html | API TestLink ] 12 | """ 13 | 14 | def __init__(self, server, devKey, testplanID = None): 15 | """ 16 | *Args:*\n 17 | _server_ - url к Testlink API;\n 18 | _devKey_ - Testlink Developer Key;\n 19 | _testplanID_ - ID тестплана; 20 | """ 21 | 22 | self.server=xmlrpclib.Server(server) 23 | self.devKey=devKey 24 | self.testplanID=testplanID 25 | 26 | def reportTCResult(self, tcid, buildid, status, testRunNotes = None): 27 | """ 28 | Сохранение результата выполнения тест-кейса в TestLink. 29 | 30 | *Args:*\n 31 | _tcid_ - ID тесткейса;\n 32 | _buildid_ - ID билда;\n 33 | _status_ - результат выполнения тесткейса ("p" - PASS, "f" - FAIL);\n 34 | _testRunNotes_ - примечание к выполенению тесткейса; 35 | 36 | *Returns:*\n 37 | Ответ от Testlink с информацией о завершении операции. 38 | """ 39 | 40 | data={"devKey":self.devKey, "testcaseexternalid":tcid, "testplanid":self.testplanID, "buildid":buildid, "status":status, "notes":testRunNotes} 41 | answer=self.server.tl.reportTCResult(data) 42 | out=answer[0] 43 | if out['message']!='Success!': 44 | raise AssertionError('Error in testlink answer. ErrorMessage:'+out['message']+' ErrorCode='+str(out['code'])) 45 | return answer 46 | 47 | def getInfo(self): 48 | """ 49 | Получение информации с сервера. 50 | 51 | *Returns:*\n 52 | Информация о Testlink. 53 | 54 | *Example:*\n 55 | Testlink API Version: 1.0 initially written by Asiel Brumfield\n 56 | with contributions by TestLink development Team 57 | """ 58 | 59 | return self.server.tl.about() 60 | 61 | def getTestPlanIdByName(self, testProjectName, testPlanName): 62 | """ 63 | Получение ID тестплана по его имени. 64 | 65 | *Args:*\n 66 | _testProjectName_ - имя проекта, в котором находятся тесты;\n 67 | _testPlanName_ - имя тестплана, в котором находятся тесты;\n 68 | 69 | *Returns:*\n 70 | ID тестплана. 71 | """ 72 | 73 | data={"devKey": self.devKey, 74 | "testprojectname": testProjectName, 75 | "testplanname": testPlanName} 76 | answer=self.server.tl.getTestPlanByName(data) 77 | out=answer[0] 78 | testlpanid=out ['id'] 79 | return testlpanid 80 | 81 | def createTestCase(self, testProjectId, testSuiteId, testCaseName, summary, authorlogin, steps = ''): 82 | """ 83 | Создание нового тесткейса. 84 | 85 | *Args:*\n 86 | _testProjectId_ - ID проекта, в который необходимо добавить тесткейс;\n 87 | _testSuiteId_ - ID тестсьюты, в которую необходимо добавить тесткейс;\n 88 | _testCaseName_ - имя тесткейса;\n 89 | _summary_ - описание тесткейса;\n 90 | _authorlogin_ - имя пользователя, от которого создаётся тесткейс;\n 91 | _steps_ - шаги выполнения тесткейса; 92 | 93 | *Returns:*\n 94 | ID созданного тесткейса. 95 | """ 96 | 97 | data={"devKey": self.devKey, 98 | "testprojectid": testProjectId, 99 | "testsuiteid": testSuiteId, 100 | "testcasename":testCaseName, 101 | "summary":summary, 102 | "authorlogin":authorlogin} 103 | data['actiononduplicatedname']='block' 104 | data['checkduplicatedname']='true' 105 | data['steps']=steps 106 | answer=self.server.tl.createTestCase(data) 107 | out=answer[0] 108 | logger.debug (out) 109 | testcaseid=out ['id'] 110 | return testcaseid 111 | 112 | def getProjectIdByName(self, projectName): 113 | """ 114 | Получение ID проекта по его имени. 115 | 116 | *Args:*\n 117 | _projectName_ - имя проекта; 118 | 119 | *Returns:*\n 120 | ID проекта.\n 121 | False в случае неудачи. 122 | """ 123 | 124 | data={ 125 | 'devKey': self.devKey 126 | } 127 | for tmp_project in self.server.tl.getProjects(data): 128 | if (tmp_project['name']==projectName): 129 | return tmp_project['id'] 130 | return False 131 | 132 | def getTestCaseInternalIdByName(self, testCaseName): 133 | """ 134 | Получение внутреннего ID тесткейса по его имени.\n 135 | Тесткейсы в Testlink имеют внутренние ID и внешние. 136 | 137 | *Args:*\n 138 | _testCaseName_ - имя тесткейса;\n 139 | 140 | *Returns:*\n 141 | ID тесткейса. 142 | 143 | *Raises:*\n 144 | "Test Case not found" в том случе, если тесткейс не найден. 145 | """ 146 | 147 | tc_byname={ 148 | "devKey": self.devKey, 149 | "testcasename": testCaseName 150 | } 151 | 152 | answer=self.server.tl.getTestCaseIDByName(tc_byname) 153 | out=answer[0] 154 | if not 'id' in answer[0]: 155 | raise AssertionError("Test Case not found") 156 | return out['id'] 157 | 158 | def createBuild(self, buildName, buildNotes): 159 | """ 160 | Создание билда. 161 | 162 | *Args:*\n 163 | _buildName_ - имя создаваемого билда;\n 164 | _buildNotes_ - примечание к создаваемому билду; 165 | 166 | *Returns:*\n 167 | ID созданного билда. 168 | """ 169 | 170 | data={"devKey":self.devKey, "testplanid":self.testplanID, "buildname":buildName, "buildnotes":buildNotes} 171 | print "Build Name "+buildName+"created with notes"+buildNotes 172 | x=self.server.tl.createBuild(data) 173 | out=x[0] 174 | buildID=out['id'] 175 | print "Build ID is %s"%buildID 176 | return (buildID) 177 | 178 | def getTestCaseIDFromTestName(self, testcaseName): 179 | """ 180 | Получение идентификатора тесткейса для TestLink из его имени в Robot Framework. 181 | Имя тесткейса в Robot Framework должно быть вида: _ 182 | 183 | *Args:*\n 184 | _testcaseName_ - имя тесткейса в Robot Framework;\n 185 | 186 | *Returns:*\n 187 | ID тесткейса для Testlink из его имени. 188 | 189 | *Example:*\n 190 | Имя тесткейса в Robot Framework: TEST-1_test case name\n 191 | => \n 192 | Идентификатор теста для TestLink: TEST-1 193 | """ 194 | 195 | x1=testcaseName.split("-") 196 | x2=x1[0] 197 | x3=x2.split("_") 198 | testcaseID=x3[1] 199 | return (testcaseID) 200 | 201 | def getTestCaseIDFromTestTags(self, tags): 202 | """ 203 | Получение идентификатора тесткейса для TestLink из тега теста в Robot Framework. 204 | Один из тегов в списке должен быть вида testlinkid= 205 | 206 | *Args:*\n 207 | _tags_ - список тегов для тесткейса в Robot Framework;\n 208 | 209 | *Returns:*\n 210 | ID тесткейса для Testlink из его тегов. 211 | 212 | *Example:*\n 213 | Тег в тесте: [Tags] testlinkid=TEST-40\n 214 | =>\n 215 | Идентификатор теста в TestLink: TEST-40 216 | """ 217 | 218 | match_tag='testlinkid' 219 | for tag in tags: 220 | match=re.match(match_tag, tag) 221 | if match : 222 | split_tag=tag.split('=') 223 | testcaseID=split_tag[1] 224 | return testcaseID 225 | 226 | def getLatestBuildForTestPlan(self): 227 | """ 228 | Получение последнего билда из тестплана. 229 | 230 | *Returns:*\n 231 | ID билда. 232 | """ 233 | 234 | data={"devKey":self.devKey, "testplanid":self.testplanID} 235 | existingBuild=self.server.tl.getLatestBuildForTestPlan(data) 236 | return existingBuild['id'] 237 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /library/OracleDB.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from robot.api import logger 4 | from robot.utils import ConnectionCache 5 | 6 | try: 7 | import cx_Oracle 8 | except ImportError,info: 9 | logger.warn ("Import cx_Oracle Error:",info) 10 | if cx_Oracle.version<'3.0': 11 | logger.warn ("Very old version of cx_Oracle :",cx_Oracle.version) 12 | 13 | class OracleDB(object): 14 | """ 15 | Библиотека для работы с базой данных Oracle. 16 | 17 | == Зависимости == 18 | | cx_Oracle | http://cx-oracle.sourceforge.net | version > 3.0 | 19 | | robot framework | http://robotframework.org | 20 | """ 21 | 22 | ROBOT_LIBRARY_SCOPE = 'GLOBAL' 23 | 24 | def __init__(self): 25 | self._connection = None 26 | self._cache = ConnectionCache() # использование кеша Robot Framework для одновременной работы с несколькими соединениями 27 | 28 | def connect_to_oracle (self, dbName, dbUserName, dbPassword, alias=None): 29 | """ 30 | Подключение к Oracle. 31 | 32 | *Args:*\n 33 | _dbName_ - имя базы данных;\n 34 | _dbUserName_ - имя пользователя;\n 35 | _dbPassword_ - пароль пользователя;\n 36 | _alias_ - псевдоним соединения;\n 37 | 38 | *Returns:*\n 39 | Индекс текущего соединения. 40 | 41 | *Example:*\n 42 | | Connect To Oracle | rb60db | bis | password | 43 | """ 44 | 45 | try: 46 | logger.debug ('Connecting using : dbName=%s, dbUserName=%s, dbPassword=%s ' % (dbName, dbUserName, dbPassword)) 47 | connection_string = '%s/%s@%s' % (dbUserName,dbPassword,dbName) 48 | self._connection=cx_Oracle.connect(connection_string) 49 | return self._cache.register(self._connection, alias) 50 | except cx_Oracle.DatabaseError,info: 51 | raise Exception ("Logon to oracle Error:",str(info)) 52 | 53 | def disconnect_from_oracle(self): 54 | """ 55 | Закрытие текущего соединения с Oracle. 56 | 57 | *Example:*\n 58 | | Connect To Oracle | rb60db | bis | password | 59 | | Disconnect From Oracle | 60 | """ 61 | 62 | self._connection.close() 63 | 64 | def close_all_oracle_connections (self): 65 | """ 66 | Закрытие всех соединений с Oracle. 67 | 68 | Данный keyword используется для закрытия всех соединений в том случае, если их было открыто несколько штук. 69 | Использовать [#Disconnect From Oracle|Disconnect From Oracle] и [#Close All Oracle Connections|Close All Oracle Connections] 70 | вместе нельзя. 71 | 72 | После выполнения этого keyword индекс, возвращаемый [#Connect To Oracle|Connect To Oracle], начинается с 1. 73 | 74 | *Example:*\n 75 | | Connect To Oracle | rb60db | bis | password | alias=bis | 76 | | Connect To Oracle | rb60db | bis_dcs | password | alias=bis_dsc | 77 | | Switch Oracle Connection | bis | 78 | | @{sql_out_bis}= | Execute Sql String | select SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') from dual | 79 | | Switch Oracle Connection | bis_dsc | 80 | | @{sql_out_bis_dsc}= | Execute Sql String | select SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') from dual | 81 | | Close All Oracle Connections | 82 | """ 83 | 84 | self._connection = self._cache.close_all() 85 | 86 | def switch_oracle_connection(self,index_or_alias): 87 | """ 88 | Переключение между активными соединениями с Oracle, используя их индекс или псевдоним. 89 | 90 | Псевдоним задается в keyword [#Connect To Oracle|Connect To Oracle], который также возвращает индекс соединения. 91 | 92 | *Args:*\n 93 | _index_or_alias_ - индекс соединения или его псевдоним; 94 | 95 | *Returns:*\n 96 | Индекс предыдущего соединения. 97 | 98 | *Example:* (switch by alias)\n 99 | | Connect To Oracle | rb60db | bis | password | alias=bis | 100 | | Connect To Oracle | rb60db | bis_dcs | password | alias=bis_dsc | 101 | | Switch Oracle Connection | bis | 102 | | @{sql_out_bis}= | Execute Sql String | select SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') from dual | 103 | | Switch Oracle Connection | bis_dsc | 104 | | @{sql_out_bis_dsc}= | Execute Sql String | select SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') from dual | 105 | | Close All Oracle Connections | 106 | =>\n 107 | @{sql_out_bis} = BIS\n 108 | @{sql_out_bis_dcs}= BIS_DCS 109 | 110 | *Example:* (switch by index)\n 111 | | ${bis_index}= | Connect To Oracle | rb60db | bis | password | 112 | | ${bis_dcs_index}= | Connect To Oracle | rb60db | bis_dcs | password | 113 | | @{sql_out_bis_dcs_1}= | Execute Sql String | select SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') from dual | 114 | | ${previous_index}= | Switch Oracle Connection | ${bis_index} | 115 | | @{sql_out_bis}= | Execute Sql String | select SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') from dual | 116 | | Switch Oracle Connection | ${previous_index} | 117 | | @{sql_out_bis_dcs_2}= | Execute Sql String | select SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') from dual | 118 | | Close All Oracle Connections | 119 | =>\n 120 | ${bis_index}= 1\n 121 | ${bis_dcs_index}= 2\n 122 | @{sql_out_bis_dcs_1} = BIS_DCS\n 123 | ${previous_index}= 2\n 124 | @{sql_out_bis} = BIS\n 125 | @{sql_out_bis_dcs_2}= BIS_DCS 126 | """ 127 | 128 | old_index = self._cache.current_index 129 | self._connection = self._cache.switch(index_or_alias) 130 | return old_index 131 | 132 | def _execute_sql (self, cursor, Statement): 133 | logger.debug("Executing :\n %s" % Statement) 134 | cursor.prepare(Statement) 135 | return cursor.execute(None) 136 | 137 | def execute_plsql_block (self,plsqlStatement): 138 | """ 139 | Выполнение PL\SQL блока. 140 | 141 | *Args:*\n 142 | _plsqlStatement_ - PL\SQL блок;\n 143 | 144 | *Raises:*\n 145 | PLSQL Error: Ошибка выполнения PL\SQL; выводится сообщение об ошибке в кодировке той БД, где выполняется код. 146 | 147 | *Example:*\n 148 | | *Settings* | *Value* | 149 | | Library | OracleDB | 150 | 151 | | *Variables* | *Value* | 152 | | ${var_failed} | 3 | 153 | 154 | | *Test Cases* | *Action* | *Argument* | *Argument* | *Argument* | 155 | | Simple | 156 | | | ${statement}= | catenate | SEPARATOR=\\r\\n | DECLARE | 157 | | | ... | | | a NUMBER := ${var_failed}; | 158 | | | ... | | | BEGIN | 159 | | | ... | | | a := a + 1; | 160 | | | ... | | | if a = 4 then | 161 | | | ... | | | raise_application_error ( -20001, 'This is a custom error' ); | 162 | | | ... | | | end if; | 163 | | | ... | | | END; | 164 | | | Execute Plsql Block | ${statement} | 165 | =>\n 166 | DatabaseError: ORA-20001: This is a custom error 167 | """ 168 | 169 | cursor = None 170 | try: 171 | cursor = self._connection.cursor() 172 | self._execute_sql (cursor,plsqlStatement) 173 | self._connection.commit() 174 | finally: 175 | if cursor: 176 | self._connection.rollback() 177 | 178 | def execute_plsql_block_with_dbms_output (self,plsqlStatement): 179 | """ 180 | Выполнение PL\SQL блока с dbms_output(). 181 | 182 | *Args:*\n 183 | _plsqlStatement_ - PL\SQL блок;\n 184 | 185 | *Raises:*\n 186 | PLSQL Error: Ошибка выполнения PL\SQL; выводится сообщение об ошибке в кодировке той БД, где выполняется код. 187 | 188 | *Returns:*\n 189 | Список с значениями из функций Oracle dbms_output.put_line(). 190 | 191 | *Example:*\n 192 | | *Settings* | *Value* | 193 | | Library | OracleDB | 194 | 195 | | *Variables* | *Value* | 196 | | ${var} | 4 | 197 | 198 | | *Test Cases* | *Action* | *Argument* | *Argument* | *Argument* | 199 | | Simple | 200 | | | ${statement}= | catenate | SEPARATOR=\\r\\n | DECLARE | 201 | | | ... | | | a NUMBER := ${var}; | 202 | | | ... | | | BEGIN | 203 | | | ... | | | a := a + 1; | 204 | | | ... | | | if a = 4 then | 205 | | | ... | | | raise_application_error ( -20001, 'This is a custom error' ); | 206 | | | ... | | | end if; | 207 | | | ... | | | dbms_output.put_line ('text '||a||', e-mail text'); | 208 | | | ... | | | dbms_output.put_line ('string 2 '); | 209 | | | ... | | | END; | 210 | | | @{dbms}= | Execute Plsql Block With Dbms Output | ${statement} | 211 | =>\n 212 | | @{dbms} | text 5, e-mail text | 213 | | | string 2 | 214 | """ 215 | 216 | cursor = None 217 | dbms_output = [] 218 | try: 219 | cursor = self._connection.cursor() 220 | cursor.callproc("dbms_output.enable") 221 | self._execute_sql (cursor,plsqlStatement) 222 | self._connection.commit() 223 | statusVar = cursor.var(cx_Oracle.NUMBER) 224 | lineVar = cursor.var(cx_Oracle.STRING) 225 | while True: 226 | cursor.callproc("dbms_output.get_line", (lineVar, statusVar)) 227 | if statusVar.getvalue() != 0: 228 | break 229 | dbms_output.append(lineVar.getvalue()) 230 | return dbms_output 231 | finally: 232 | if cursor: 233 | self._connection.rollback() 234 | 235 | def execute_sql_string (self,plsqlStatement): 236 | """ 237 | Выполнение SQL выборки из БД. 238 | 239 | *Args:*\n 240 | _plsqlStatement_ - PL\SQL блок;\n 241 | 242 | *Raises:*\n 243 | PLSQL Error: Ошибка выполнения PL\SQL; выводится сообщение об ошибке в кодировке той БД, где выполняется код. 244 | 245 | *Returns:*\n 246 | Выборка в виде таблицы. 247 | 248 | *Example:*\n 249 | | @{query}= | Execute Sql String | select sysdate, sysdate+1 from dual | 250 | | Set Test Variable | ${sys_date} | ${query[0][0]} | 251 | | Set Test Variable | ${next_date} | ${query[0][1]} | 252 | """ 253 | 254 | cursor = None 255 | try: 256 | cursor = self._connection.cursor() 257 | self._execute_sql (cursor,plsqlStatement) 258 | return cursor.fetchall() 259 | finally: 260 | if cursor: 261 | self._connection.rollback() 262 | -------------------------------------------------------------------------------- /listener/LogGrabber.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from robot.libraries.BuiltIn import BuiltIn 4 | from SSHLibrary import SSHLibrary 5 | from AdvancedLogging import AdvancedLogging 6 | import zipfile 7 | import os 8 | import shutil 9 | import string 10 | 11 | class LogGrabber(object): 12 | """ 13 | Получение логов подсистем с удаленных серверов. \n 14 | Принцип работы: 15 | - перед началом теста происходит подсчет количества строк в логах 16 | - после окончания теста, если количество строк изменилось, то будут скачены только добавленные в лог строки. 17 | Для работы библиотеки необходимо 18 | cоздать переменную server_logs в python [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#creating-variables-directly|variable file] 19 | следующего вида: 20 | | server_logs = { 21 | | "tmpdir": "/tmp/", 22 | | "servers": [ 23 | | { 24 | | "hostname": "server.com", 25 | | "port": 22, 26 | | "username": "my_username", 27 | | "password": "my_password_to_ssh", 28 | | "subsystems": [ 29 | | { 30 | | "name": "Apache_server", 31 | | "logs": [ 32 | | { 33 | | "path_to_log": "/var/log", 34 | | "log_name": "access.log" 35 | | }, 36 | | { 37 | | "path_to_log": "/var/log", 38 | | "log_name": "error*.log" 39 | | } 40 | | ] 41 | | } 42 | | ] 43 | | } 44 | | ] 45 | | } 46 | Где: 47 | - tmpdir - каталог для временных файлов на удаленном сервере 48 | - hostname - имя хоста удаленного сервера 49 | - port - порт подключения по ssh 50 | - username\password - логин\пароль для подключения по ssh 51 | - name - имя подсистемы, для которой собираются логи, должно быть уникальным 52 | - path_to_log - путь к логам подсистемы 53 | - log_name - имя файла лога; могут использоваться wildcards аналогичные тем, что применяются в linux-команде find. 54 | 55 | === Ограничения === 56 | Логи подсистем должны находится на Linux сервере с возможностью подключения к нему по ssh. 57 | 58 | === Использование === 59 | 1. В качестве [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#using-listener-interface|listener]:\n 60 | ``pybot --listener LogGrabber /path/to/test_suite``\n 61 | При этом нет необходимости изменять тесты. 62 | После завершения теста со статусом FAILED будет скачена лишь та часть логов, которая была записана во время его проходжения. 63 | 64 | 2. В качестве библиотеки:\n 65 | В этом случае можно скачивать логи вне зависимости от статуса теста 66 | | *** Settings *** 67 | | Documentation Пример 68 | | Library LogGrabber 69 | | Variables variables.py 70 | | Suite Setup SuiteSetup 71 | | Suite Teardown SuiteTeardown 72 | | 73 | | *** Test Cases *** 74 | | Fail_test 75 | | FAIL fail message 76 | | 77 | | Passed_test 78 | | Pass Execution passed message 79 | | 80 | | 81 | | *** Keywords *** 82 | | SuiteSetup 83 | | LogGrabber.Set connections 84 | | LogGrabber.Prepare logs 85 | | 86 | | 87 | | SuiteTeardown 88 | | LogGrabber.Download logs 89 | | LogGrabber.Close connections 90 | 91 | === Зависимости === 92 | | robot framework | http://robotframework.org | 93 | | AdvancedLogging | http://git.billing.ru/cgit/PS_RF.git/tree/library/AdvancedLogging.py | 94 | | SSHLibrary | http://robotframework.org/SSHLibrary/latest/SSHLibrary.html | 95 | 96 | """ 97 | ROBOT_LIBRARY_SCOPE = 'GLOBAL' 98 | ROBOT_LISTENER_API_VERSION = 2 99 | 100 | def __init__(self): 101 | # загрузка встроенных библиотек 102 | self.bi=BuiltIn() 103 | self.ssh=SSHLibrary() 104 | self.adv_log=AdvancedLogging() 105 | 106 | # словарь с подготовленными логами 107 | self.prepared_logs = dict() 108 | 109 | def start_suite(self, name, attrs): 110 | self.set_connections() 111 | 112 | def start_test(self, name, attrs): 113 | self.prepare_logs() 114 | 115 | def end_test(self, name, attrs): 116 | if attrs['status'] != 'PASS': 117 | self.download_logs() 118 | 119 | def end_suite(self, name, attrs): 120 | self.close_connections() 121 | 122 | def set_connections(self): 123 | """ 124 | SSH-соединение с удаленными серверами 125 | """ 126 | 127 | # Получаем информацию о логах подсистем 128 | self.logs=self.bi.get_variable_value('${server_logs}') 129 | # Системный разделитель для платформы запуска тестов 130 | self.sys_separator=self.bi.get_variable_value('${/}') 131 | # Разделитель в unix 132 | self.nix_separator = '/' 133 | 134 | # перебираем сервера из словаря настройки 135 | # для каждого сервера создаем одтельное ssh-соединение, 136 | # которое будет жить в течение всего suite 137 | # дописываем alias в словарь для каждого сервера 138 | for _, server in enumerate(self.logs["servers"]): 139 | hostname = server["hostname"] 140 | port = server["port"] 141 | username = server["username"] 142 | password = server["password"] 143 | ssh_alias = str(self.bi.get_time('epoch')) 144 | # создаем ssh-соединение - alias = epoch timestamp 145 | self.ssh.open_connection(hostname, ssh_alias, port) 146 | self.ssh.login(username, password) 147 | server["ssh_alias"] = ssh_alias 148 | 149 | def prepare_logs(self): 150 | """ 151 | Подготовка логов. 152 | В результате для каждого лога, удовлетворяющего настройке, 153 | записывается номер послнедней строки. 154 | 155 | """ 156 | 157 | # структура с описанием серверов, подсистем и логов 158 | self.prepared_logs["servers"] = [] 159 | # перебираем сервера из конфигурации 160 | for server in self.logs["servers"]: 161 | processed_server = dict() 162 | hostname = server["hostname"] 163 | port = server["port"] 164 | username = server["username"] 165 | password = server["password"] 166 | ssh_alias = server["ssh_alias"] 167 | # заполняем словарь, описывающий обработанный сервер 168 | processed_server["hostname"] = hostname 169 | processed_server["port"] = port 170 | processed_server["username"] = username 171 | processed_server["password"] = password 172 | processed_server["ssh_alias"] = ssh_alias 173 | processed_server["subsystems"] = [] 174 | # переключаемся на соединение с alias = ssh_alias из словаря настройки 175 | self.ssh.switch_connection(ssh_alias) 176 | # для каждого сервера обрабатываем набор подсистем 177 | for subsystem in server["subsystems"]: 178 | # словарь обработанных подсистем 179 | processed_subsys = dict() 180 | processed_subsys["name"] = subsystem["name"] 181 | # список обработанных логов 182 | processed_logs = [] 183 | # обрабатываем логи для текущей подсистемы 184 | for subsys_log in subsystem["logs"]: 185 | path_to_log = subsys_log["path_to_log"] 186 | log_name_regexp = subsys_log["log_name"] 187 | # получаем список лог-файлов по regexp 188 | log_name_list_text = self.ssh.execute_command("find {}{}{} -printf '%f\n'".format(path_to_log, self.nix_separator, log_name_regexp), True, True, True) 189 | # если список не пуст и код возврата команды 0 (success) 190 | if ((len(log_name_list_text[0]) > 0) & (log_name_list_text[2] == 0) ): 191 | # формируем массив имен лог-файлов 192 | log_name_array = string.split(log_name_list_text[0], '\n') 193 | # для каждого файла получаем номер последней строки 194 | for log_name in log_name_array: 195 | line_number = self.ssh.execute_command("cat {}{}{} | wc -l".format(path_to_log, self.nix_separator, log_name)) 196 | processed_logs.append({"path_to_log": path_to_log, "log_name": log_name, "line_number": line_number}) 197 | # проверка для исключения "мусора" processed_subsys 198 | if (len(processed_logs)>0): 199 | processed_subsys["logs"] = processed_logs 200 | processed_server["subsystems"].append(processed_subsys) 201 | # проверка - есть ли для сервера обработанные подсистемы с логами 202 | if (len(processed_server["subsystems"])>0): 203 | self.prepared_logs["servers"].append(processed_server) 204 | 205 | def download_logs(self): 206 | """ 207 | Формирование и загрузка логов. 208 | В результате в директории теста, созданной AdvancedLogging, 209 | получаем архив с логами [TIMESTAMP]_logs.zip 210 | 211 | """ 212 | timestamp = self.bi.get_time('epoch') 213 | # базовая директория теста 214 | base_dir = self.adv_log.Create_Advanced_Logdir() 215 | # имя результирующего архива с логами 216 | res_arc_name = os.path.join(base_dir, "{}_logs".format(timestamp)) 217 | # результирующая директория для логов 218 | logs_dir = os.path.join(base_dir, "logs") 219 | # временная директория на целевом сервере 220 | temp_dir = self.logs['tmpdir'] 221 | # обрабатыаем сервера, с подготовленными логами 222 | for server in self.prepared_logs["servers"]: 223 | # параметры подключения к серверу 224 | ssh_alias = server["ssh_alias"] 225 | # переключаемся на соединение с alias = ssh_alias из словаря 226 | self.ssh.switch_connection(ssh_alias) 227 | # обрабатываем подсистемы с подготовленными логами 228 | for subsystem in server["subsystems"]: 229 | # структура в которую скачиваются логи [Advanced_Logdir]/logs/<подсистема>/ 230 | subsys_dir = os.path.join(logs_dir, subsystem["name"]) 231 | for log in subsystem["logs"]: 232 | abs_log_name = "{}{}{}".format(log["path_to_log"], self.nix_separator, log["log_name"]) 233 | # имя файла содержащего интересующую нас часть, лога 234 | cut_log_name = "{}_{}".format(timestamp, log["log_name"]) 235 | # абсолютный пусть с именем файла (cut_[имя_лога]) - для интересующего нас куска лога 236 | cut_abs_log_name = "{}{}{}".format(temp_dir, self.nix_separator, cut_log_name) 237 | # текущий номер строки в логе 238 | cur_line_number = self.ssh.execute_command("cat {} | wc -l".format(abs_log_name)) 239 | # проверяем, появились ли строки в логе с момента подготовки логов 240 | if (cur_line_number > log["line_number"]): 241 | # вырезаем часть лога, начиная с сохраненного номера строки 242 | self.ssh.execute_command("tail -n +{} {} > {}".format(log["line_number"], abs_log_name, cut_abs_log_name)) 243 | # gzip 244 | self.ssh.execute_command("gzip {}.gz {}".format(cut_abs_log_name, cut_abs_log_name)) 245 | # скачиваем файл 246 | self.ssh.get_file("{}.gz".format(cut_abs_log_name), "{}{}{}.gz".format(subsys_dir, self.sys_separator, cut_log_name)) 247 | # удаляем вырезанную чыасть лога и gz-архив этой части 248 | self.ssh.execute_command("rm {}".format(cut_abs_log_name)) 249 | self.ssh.execute_command("rm {}.gz".format(cut_abs_log_name )) 250 | # если есть результат - упаковываем в единый zip-архив и удаляем папку с логами 251 | if (os.path.exists(logs_dir)): 252 | self._zip(logs_dir, res_arc_name) 253 | shutil.rmtree(logs_dir) 254 | 255 | def close_connections(self): 256 | """ 257 | Закрытие ssh-соединений с удаленными серверами 258 | """ 259 | self.ssh.close_all_connections() 260 | 261 | def _zip(self, src, dst): 262 | """ 263 | Упаковка логов единый zip-архив 264 | 265 | *Args:*\n 266 | _src_ - директория для упаковки 267 | _dst_ - имя лога 268 | 269 | """ 270 | zf = zipfile.ZipFile("%s.zip" % (dst), "w", zipfile.ZIP_DEFLATED) 271 | abs_src = os.path.abspath(src) 272 | for root, _, files in os.walk(src): 273 | for filename in files: 274 | abs_name = os.path.abspath(os.path.join(root, filename)) 275 | arc_name = abs_name[len(abs_src) + 1:] 276 | zf.write(abs_name, arc_name) 277 | zf.close() -------------------------------------------------------------------------------- /library/JsonValidator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import jsonschema 5 | from jsonpath_rw import parse 6 | from jsonselect import jsonselect 7 | 8 | class JsonValidator(object): 9 | """ 10 | Библиотека для проверки json. 11 | Основана на: JSONSchema, JSONPath, JSONSelect. 12 | 13 | == Дополнительная информация == 14 | - [ http://json-schema.org/ | Json Schema ] 15 | - [ http://www.jsonschema.net/ | Jsonschema generator ] 16 | - [ http://goessner.net/articles/JsonPath/ | JSONPath by Stefan Goessner ] 17 | - [ http://jsonpath.curiousconcept.com/ | JSONPath Tester ] 18 | - [ http://jsonselect.org/ | JSONSelect] 19 | - [ http://jsonselect.curiousconcept.com/ | JSONSelect Tester] 20 | 21 | == Зависимости == 22 | | jsonschema | https://pypi.python.org/pypi/jsonschema | 23 | | jsonpath-rw | https://pypi.python.org/pypi/jsonpath-rw | 24 | | jsonselect | https://pypi.python.org/pypi/jsonselect | 25 | 26 | == Пример использования == 27 | Пример json, записанного в файле json_example.json 28 | | { "store": { 29 | | "book": [ 30 | | { "category": "reference", 31 | | "author": "Nigel Rees", 32 | | "title": "Sayings of the Century", 33 | | "price": 8.95 34 | | }, 35 | | { "category": "fiction", 36 | | "author": "Evelyn Waugh", 37 | | "title": "Sword of Honour", 38 | | "price": 12.99 39 | | }, 40 | | { "category": "fiction", 41 | | "author": "Herman Melville", 42 | | "title": "Moby Dick", 43 | | "isbn": "0-553-21311-3", 44 | | "price": 8.99 45 | | }, 46 | | { "category": "fiction", 47 | | "author": "J. R. R. Tolkien", 48 | | "title": "The Lord of the Rings", 49 | | "isbn": "0-395-19395-8", 50 | | "price": 22.99 51 | | } 52 | | ], 53 | | "bicycle": { 54 | | "color": "red", 55 | | "price": 19.95 56 | | } 57 | | } 58 | | } 59 | 60 | | *Settings* | *Value* | 61 | | Library | JsonValidator | 62 | | Library | OperatingSystem | 63 | | *Test Cases* | *Action* | *Argument* | *Argument* | 64 | | Check element | ${json_example}= | OperatingSystem.Get File | ${CURDIR}${/}json_example.json | 65 | | | Element should exist | ${json_example} | .author:contains("Evelyn Waugh") | 66 | """ 67 | 68 | ROBOT_LIBRARY_SCOPE='GLOBAL' 69 | 70 | def _validate_json(self, checked_json, schema): 71 | """ 72 | Проверка json по JSONSchema 73 | """ 74 | 75 | try: 76 | jsonschema.validate(checked_json, schema) 77 | except jsonschema.ValidationError , e: 78 | raise JsonValidatorError ('Element: %s. Error: %s. '%(e.path[0], e.message)) 79 | except jsonschema.SchemaError , e: 80 | raise JsonValidatorError ('Json-schema error:'+e.message) 81 | 82 | def validate_jsonschema_from_file (self, json_string, path_to_schema): 83 | """ 84 | Проверка json по схеме, загружаемой из файла. 85 | 86 | *Args:*\n 87 | _json_string_ - json-строка;\n 88 | _path_to_schema_ - путь к файлу со схемой json; 89 | 90 | *Raises:*\n 91 | JsonValidatorError 92 | 93 | *Example:*\n 94 | | *Settings* | *Value* | 95 | | Library | JsonValidator | 96 | | *Test Cases* | *Action* | *Argument* | *Argument* | 97 | | Simple | Validate jsonschema from file | {"foo":bar} | ${CURDIR}${/}schema.json | 98 | """ 99 | 100 | schema=open(path_to_schema).read() 101 | load_input_json=self.string_to_json (json_string) 102 | 103 | try: 104 | load_schema=json.loads(schema) 105 | except ValueError, e: 106 | raise JsonValidatorError ('Error in schema: '+e.message) 107 | 108 | self._validate_json (load_input_json, load_schema) 109 | 110 | def validate_jsonschema (self, json_string, input_schema): 111 | """ 112 | Проверка json по схеме. 113 | 114 | *Args:*\n 115 | _json_string_ - json-строка;\n 116 | _input_schema_ - схема в виде строки; 117 | 118 | *Raises:*\n 119 | JsonValidatorError 120 | 121 | *Example:*\n 122 | | *Settings* | *Value* | 123 | | Library | JsonValidator | 124 | | Library | OperatingSystem | 125 | | *Test Cases* | *Action* | *Argument* | *Argument* | 126 | | Simple | ${schema}= | OperatingSystem.Get File | ${CURDIR}${/}schema_valid.json | 127 | | | Validate jsonschema | {"foo":bar} | ${schema} | 128 | """ 129 | 130 | load_input_json=self.string_to_json (json_string) 131 | 132 | try: 133 | load_schema=json.loads(input_schema) 134 | except ValueError, e: 135 | raise JsonValidatorError ('Error in schema: '+e.message) 136 | 137 | self._validate_json (load_input_json, load_schema) 138 | 139 | def string_to_json (self, source): 140 | """ 141 | Десериализация строки в json структуру. 142 | 143 | *Args:*\n 144 | _source_ - json-строка 145 | 146 | *Return:*\n 147 | Json структура 148 | 149 | *Raises:*\n 150 | JsonValidatorError 151 | 152 | *Example:*\n 153 | | *Settings* | *Value* | 154 | | Library | JsonValidator | 155 | | Library | OperatingSystem | 156 | | *Test Cases* | *Action* | *Argument* | *Argument* | 157 | | String to json | ${json_string}= | OperatingSystem.Get File | ${CURDIR}${/}json_example.json | 158 | | | ${json}= | String to json | ${json_string} | 159 | | | Log | ${json["store"]["book"][0]["price"]} | 160 | =>\n 161 | 8.95 162 | """ 163 | 164 | try: 165 | load_input_json=json.loads(source) 166 | except ValueError, e: 167 | raise JsonValidatorError("Could not parse '%s' as JSON: %s"%(source, e)) 168 | return load_input_json 169 | 170 | def json_to_string (self, source): 171 | """ 172 | Cериализация json структуры в строку. 173 | 174 | *Args:*\n 175 | _source_ - json структура 176 | 177 | *Return:*\n 178 | Json строка 179 | 180 | *Raises:*\n 181 | JsonValidatorError 182 | 183 | *Example:*\n 184 | | *Settings* | *Value* | 185 | | Library | JsonValidator | 186 | | Library | OperatingSystem | 187 | | *Test Cases* | *Action* | *Argument* | *Argument* | 188 | | Json to string | ${json_string}= | OperatingSystem.Get File | ${CURDIR}${/}json_example.json | 189 | | | ${json}= | String to json | ${json_string} | 190 | | | ${string}= | Json to string | ${json} | 191 | | | ${pretty_string}= | Pretty print json | ${string} | 192 | | | Log to console | ${pretty_string} | 193 | """ 194 | 195 | try: 196 | load_input_json=json.dumps(source) 197 | except ValueError, e: 198 | raise JsonValidatorError("Could serialize '%s' to JSON: %s"%(source, e)) 199 | return load_input_json 200 | 201 | def get_elements (self, json_string, expr): 202 | """ 203 | Возвращает список элементов из _json_string_, соответствующих [http://goessner.net/articles/JsonPath/|JSONPath] выражению. 204 | 205 | *Args:*\n 206 | _json_string_ - json-строка;\n 207 | _expr_ - JSONPath выражение; 208 | 209 | *Return:*\n 210 | Список найденных элементов. Если элементы не найдены, то возвращается ``None`` 211 | 212 | *Example:*\n 213 | | *Settings* | *Value* | 214 | | Library | JsonValidator | 215 | | Library | OperatingSystem | 216 | | *Test Cases* | *Action* | *Argument* | *Argument* | 217 | | Get json elements | ${json_example}= | OperatingSystem.Get File | ${CURDIR}${/}json_example.json | 218 | | | ${json_elements}= | Get elements | ${json_example} | $.store.book[*].author | 219 | =>\n 220 | | [u'Nigel Rees', u'Evelyn Waugh', u'Herman Melville', u'J. R. R. Tolkien'] 221 | """ 222 | 223 | load_input_json=self.string_to_json (json_string) 224 | # парсинг jsonpath 225 | jsonpath_expr=parse(expr) 226 | # список возвращаемых элементов 227 | value_list=[] 228 | for match in jsonpath_expr.find(load_input_json): 229 | value_list.append(match.value) 230 | if not value_list: 231 | return None 232 | else: 233 | return value_list 234 | 235 | def select_elements (self, json_string, expr): 236 | """ 237 | Возвращает список элементов из _json_string_, соответствующих [ http://jsonselect.org/ | JSONSelect] выражению. 238 | 239 | *Args:*\n 240 | _json_string_ - json-строка;\n 241 | _expr_ - JSONSelect выражение; 242 | 243 | *Return:*\n 244 | Список найденных элементов. Если элементы не найдены, то ``None`` 245 | 246 | *Example:*\n 247 | | *Settings* | *Value* | 248 | | Library | JsonValidator | 249 | | Library | OperatingSystem | 250 | | *Test Cases* | *Action* | *Argument* | *Argument* | 251 | | Select json elements | ${json_example}= | OperatingSystem.Get File | ${CURDIR}${/}json_example.json | 252 | | | ${json_elements}= | Select elements | ${json_example} | .author:contains("Evelyn Waugh")~.price | 253 | =>\n 254 | | 12.99 255 | """ 256 | 257 | load_input_json=self.string_to_json (json_string) 258 | # парсинг jsonselect 259 | jsonselect.Parser(load_input_json) 260 | values=jsonselect.select(expr, load_input_json) 261 | return values 262 | 263 | def element_should_exist (self, json_string, expr): 264 | """ 265 | Проверка существования одного или более элементов, соответствующих [ http://jsonselect.org/ | JSONSelect] выражению. 266 | 267 | *Args:*\n 268 | _json_string_ - json-строка;\n 269 | _expr_ - jsonpath выражение;\n 270 | 271 | *Raises:*\n 272 | JsonValidatorError 273 | 274 | *Example:*\n 275 | | *Settings* | *Value* | 276 | | Library | JsonValidator | 277 | | Library | OperatingSystem | 278 | | *Test Cases* | *Action* | *Argument* | *Argument* | 279 | | Check element | ${json_example}= | OperatingSystem.Get File | ${CURDIR}${/}json_example.json | 280 | | | Element should exist | ${json_example} | $..book[?(@.author=='Herman Melville')] | 281 | """ 282 | 283 | value=self.select_elements (json_string, expr) 284 | if value is None: 285 | raise JsonValidatorError ('Elements %s does not exist'%expr) 286 | 287 | def element_should_not_exist (self, json_string, expr): 288 | """ 289 | Проверка отсутствия одного или более элементов, соответствующих [ http://jsonselect.org/ | JSONSelect] выражению. 290 | 291 | *Args:*\n 292 | _json_string_ - json-строка;\n 293 | _expr_ - jsonpath выражение;\n 294 | 295 | *Raises:*\n 296 | JsonValidatorError 297 | """ 298 | 299 | value=self.select_elements (json_string, expr) 300 | if value is not None: 301 | raise JsonValidatorError ('Elements %s exist but should not'%expr) 302 | 303 | def update_json(self, json_string, expr, value, index=0): 304 | """ 305 | Замена значения в json-строке. 306 | 307 | *Args:*\n 308 | _json_string_ - json-строка dict;\n 309 | _expr_ - JSONPath выражение для определения заменяемого значения;\n 310 | _value_ - значение, на которое будет произведена замена;\n 311 | _index_ - устанавливает индекс для выбора элемента внутри списка совпадений, по-умолчанию равен 0;\n 312 | 313 | *Return:*\n 314 | Изменённый json в виде словаря. 315 | 316 | *Example:*\n 317 | | *Settings* | *Value* | 318 | | Library | JsonValidator | 319 | | Library | OperatingSystem | 320 | | *Test Cases* | *Action* | *Argument* | *Argument* | 321 | | Update element | ${json_example}= | OperatingSystem.Get File | ${CURDIR}${/}json_example.json | 322 | | | ${json_update}= | Update_json | ${json_example} | $..color | changed | 323 | """ 324 | 325 | load_input_json=self.string_to_json (json_string) 326 | matches = self._json_path_search(load_input_json, expr) 327 | 328 | datum_object = matches[int(index)] 329 | 330 | if not isinstance(datum_object, DatumInContext): 331 | raise JsonValidatorError("Nothing found by the given json-path") 332 | 333 | path = datum_object.path 334 | 335 | # Изменить справочник используя полученные данные 336 | # Если пользователь указал на список 337 | if isinstance(path, Index): 338 | datum_object.context.value[datum_object.path.index] = value 339 | # Если пользователь указал на значение (string, bool, integer or complex) 340 | elif isinstance(path, Fields): 341 | datum_object.context.value[datum_object.path.fields[0]] = value 342 | 343 | return load_input_json 344 | 345 | def pretty_print_json (self, json_string): 346 | """ 347 | Возврещает отформатированную json-строку _json_string_.\n 348 | Используется метод json.dumps с настройкой _indent=2, ensure_ascii=False_. 349 | 350 | *Args:*\n 351 | _json_string_ - json-строка. 352 | 353 | *Example:*\n 354 | | *Settings* | *Value* | 355 | | Library | JsonValidator | 356 | | Library | OperatingSystem | 357 | | *Test Cases* | *Action* | *Argument* | *Argument* | 358 | | Check element | ${pretty_json}= | Pretty print json | {a:1,foo:[{b:2,c:3},{d:"baz",e:4}]} | 359 | | | Log | ${pretty_json} | 360 | =>\n 361 | | { 362 | | "a": 1, 363 | | "foo": [ 364 | | { 365 | | "c": 3, 366 | | "b": 2 367 | | }, 368 | | { 369 | | "e": 4, 370 | | "d": "baz" 371 | | } 372 | | ] 373 | | } 374 | """ 375 | 376 | return json.dumps(self.string_to_json(json_string), indent=2, ensure_ascii=False) 377 | 378 | class JsonValidatorError(Exception): 379 | pass 380 | -------------------------------------------------------------------------------- /library/RabbitMqManager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from robot.api import logger 4 | from robot.utils import ConnectionCache 5 | import httplib 6 | import base64 7 | import json 8 | import socket 9 | import urlparse 10 | import urllib 11 | 12 | 13 | class RabbitMqManager(object): 14 | """ 15 | Библиотека для управления сервером RabbitMq. 16 | 17 | Реализована на основе: 18 | - [ http://hg.rabbitmq.com/rabbitmq-management/raw-file/3646dee55e02/priv/www-api/help.html | RabbitMQ Management HTTP API ] 19 | - [ https://github.com/rabbitmq/rabbitmq-management/blob/master/bin/rabbitmqadmin | rabbitmqadmin ] 20 | 21 | == Зависимости == 22 | | robot framework | http://robotframework.org | 23 | 24 | == Example == 25 | | *Settings* | *Value* | 26 | | Library | RabbitMqManager | 27 | | Library | Collections | 28 | 29 | | *Test Cases* | *Action* | *Argument* | *Argument* | *Argument* | *Argument* | *Argument* | 30 | | Simple | 31 | | | Connect To Rabbitmq | my_host_name | 15672 | guest | guest | alias=rmq | 32 | | | ${overview}= | Overview | 33 | | | Log Dictionary | ${overview} | 34 | | | Close All Rabbitmq Connections | 35 | """ 36 | 37 | ROBOT_LIBRARY_SCOPE='GLOBAL' 38 | 39 | def __init__(self): 40 | self._connection=None 41 | self.headers=None 42 | self._cache=ConnectionCache() 43 | 44 | def connect_to_rabbitmq (self, host, port, username = 'guest', password = 'guest', timeout = 15, alias = None): 45 | """ 46 | Подключение к серверу RabbitMq. 47 | 48 | *Args:*\n 49 | _host_ - имя сервера;\n 50 | _port_ - номер порта;\n 51 | _username_ - имя пользователя;\n 52 | _password_ - пароль пользователя;\n 53 | _timeout_ - время ожидания соединения;\n 54 | _alias_ - псевдоним соединения;\n 55 | 56 | *Returns:*\n 57 | Индекс текущего соединения. 58 | 59 | *Raises:*\n 60 | socket.error в том случае, если невозможно создать соединение. 61 | 62 | *Example:*\n 63 | | Connect To Rabbitmq | my_host_name | 15672 | guest | guest | alias=rmq | 64 | """ 65 | 66 | port=int (port) 67 | timeout=int (timeout) 68 | logger.debug ('Connecting using : host=%s, port=%d, username=%s, password=%s, timeout=%d, alias=%s '%(host, port, username, password, timeout, alias)) 69 | self.headers={"Authorization":"Basic "+base64.b64encode(username+":"+password)} 70 | try: 71 | self._connection=httplib.HTTPConnection (host, port, timeout) 72 | self._connection.connect() 73 | return self._cache.register(self._connection, alias) 74 | except socket.error, e: 75 | raise Exception ("Could not connect to RabbitMq", str(e)) 76 | 77 | def switch_rabbitmq_connection (self, index_or_alias): 78 | """ 79 | Переключение между активными соединениями с RabbitMq, используя их индекс или псевдоним. 80 | 81 | Псевдоним задается в keyword [#Connect To Rabbitmq|Connect To Rabbitmq], который также возвращает индекс соединения. 82 | 83 | *Args:*\n 84 | _index_or_alias_ -индекс соединения или его псевдоним; 85 | 86 | *Returns:*\n 87 | Индекс предыдущего соединения. 88 | 89 | *Example:*\n 90 | | Connect To Rabbitmq | my_host_name_1 | 15672 | guest | guest | alias=rmq1 | 91 | | Connect To Rabbitmq | my_host_name_2 | 15672 | guest | guest | alias=rmq2 | 92 | | Switch Rabbitmq Connection | rmq1 | 93 | | ${live}= | Is alive | 94 | | Switch Rabbitmq Connection | rmq2 | 95 | | ${live}= | Is alive | 96 | | Close All Rabbitmq Connections | 97 | """ 98 | 99 | old_index=self._cache.current_index 100 | self._connection=self._cache.switch(index_or_alias) 101 | return old_index 102 | 103 | def disconnect_from_rabbitmq(self): 104 | """ 105 | Закрытие текущего соединения с RabbitMq. 106 | 107 | *Example:*\n 108 | | Connect To Rabbitmq | my_host_name | 15672 | guest | guest | alias=rmq | 109 | | Disconnect From Rabbitmq | 110 | """ 111 | 112 | logger.debug ('Close connection with : host=%s, port=%d '%(self._connection.host, self._connection.port)) 113 | self._connection.close() 114 | 115 | def close_all_rabbitmq_connections (self): 116 | """ 117 | Закрытие всех соединений с RabbitMq. 118 | 119 | Данный keyword используется для закрытия всех соединений в том случае, если их было открыто несколько штук. 120 | Использовать [#Disconnect From Rabbitmq|Disconnect From Rabbitmq] и [#Close All Rabbitmq Connections|Close All Rabbitmq Connections] 121 | вместе нельзя. 122 | 123 | После выполнения этого keyword индекс, возвращаемый [#Connect To Rabbitmq|Connect To Rabbitmq], начинается с 1. 124 | 125 | *Example:*\n 126 | | Connect To Rabbitmq | my_host_name | 15672 | guest | guest | alias=rmq | 127 | | Close All Rabbitmq Connections | 128 | """ 129 | 130 | self._connection=self._cache.close_all() 131 | 132 | def _http_request (self, method, path, body): 133 | """ 134 | Выполнение запросов к RabbitMq 135 | 136 | *Args:*\n 137 | _method_ - метод запроса;\n 138 | _path_ - uri запроса;\n 139 | _body_ - тело POST-запроса;\n 140 | """ 141 | 142 | if body!="": 143 | self.headers["Content-Type"]="application/json" 144 | 145 | logger.debug ('Prepared request with metod '+method+' to '+'http://'+self._connection.host+':'+str(self._connection.port)+path+' and body\n'+body) 146 | 147 | try: 148 | self._connection.request(method, path, body, self.headers) 149 | except socket.error, e: 150 | raise Exception("Could not send request: {0}".format(e)) 151 | 152 | resp=self._connection.getresponse() 153 | 154 | if resp.status==400: 155 | raise Exception (json.loads(resp.read())['reason']) 156 | if resp.status==401: 157 | raise Exception("Access refused: {0}".format('http://'+self._connection.host+':'+str(self._connection.port)+path)) 158 | if resp.status==404: 159 | raise Exception("Not found: {0}".format('http://'+self._connection.host+':'+str(self._connection.port)+path)) 160 | if resp.status==301: 161 | url=urlparse.urlparse(resp.getheader('location')) 162 | [host, port]=url.netloc.split(':') 163 | self.options.hostname=host 164 | self.options.port=int(port) 165 | return self.http(method, url.path+'?'+url.query, body) 166 | if resp.status<200 or resp.status>400: 167 | raise Exception("Received %d %s for request %s\n%s" 168 | %(resp.status, resp.reason, 'http://'+self._connection.host+':'+str(self._connection.port)+path, resp.read())) 169 | return resp.read() 170 | 171 | def _get (self, path): 172 | return self._http_request('GET', '/api%s'%path, '') 173 | 174 | def _put (self, path, body): 175 | return self._http_request("PUT", "/api%s"%path, body) 176 | 177 | def _post (self, path, body): 178 | return self._http_request("POST", "/api%s"%path, body) 179 | 180 | def _delete (self, path): 181 | return self._http_request("DELETE", "/api%s"%path, "") 182 | 183 | def _quote_vhost (self, vhost): 184 | """ 185 | Декодирование vhost. 186 | """ 187 | 188 | if vhost=='/': 189 | vhost='%2F' 190 | if vhost!='%2F': 191 | vhost=urllib.quote(vhost) 192 | return vhost 193 | 194 | def is_alive(self): 195 | """ 196 | Проверка работоспособности RabbitMq. 197 | 198 | Отправляется GET-запрос следующего вида: 'http://:/api/' и проверяется код возврата. 199 | 200 | *Returns:*\n 201 | bool True, если код возврата равен 200.\n 202 | bool False во всех остальных случаях. 203 | 204 | *Raises:*\n 205 | socket.error в том случае, если невозмодно отправить GET-запрос. 206 | 207 | *Example:*\n 208 | | ${live}= | Is Alive | 209 | =>\n 210 | True 211 | """ 212 | 213 | try: 214 | self._connection.request('GET', '/api/') 215 | except socket.error, e: 216 | raise Exception("Could not send request: {0}".format(e)) 217 | resp=self._connection.getresponse() 218 | resp.read() 219 | logger.debug ('Response status=%d'%resp.status) 220 | if resp.status==200 : 221 | return True 222 | else: 223 | return False 224 | 225 | def overview (self): 226 | """ 227 | Информация о сервере RabbitMq. 228 | 229 | *Returns:*\n 230 | Словарь с информацией о сервере. 231 | 232 | *Example:*\n 233 | | ${overview}= | Overview | 234 | | Log Dictionary | ${overview} | 235 | | ${version}= | Get From Dictionary | ${overview} | rabbitmq_version | 236 | =>\n 237 | Dictionary size is 13 and it contains following items: 238 | | erlang_full_version | Erlang R16B02 (erts-5.10.3) [source] [64-bit] [smp:2:2] [async-threads:30] [hipe] [kernel-poll:true] | 239 | | erlang_version | R16B02 | 240 | | listeners | [{u'node': u'rabbit@srv2-rs582b-m', u'ip_address': u'0.0.0.0', u'protocol': u'amqp', u'port': 5672}] | 241 | | management_version | 3.1.0 | 242 | | message_stats | [] | 243 | 244 | ${version} = 3.1.0 245 | """ 246 | 247 | return json.loads(self._get ('/overview')) 248 | 249 | def connections (self): 250 | """ 251 | Список открытых соединений. 252 | """ 253 | 254 | return json.loads(self._get ('/connections')) 255 | 256 | def get_name_of_all_connections (self): 257 | """ 258 | Список имен всех открытых соединений. 259 | """ 260 | 261 | names=[] 262 | data=self.connections () 263 | for item in data : 264 | names.append(item['name']) 265 | return names 266 | 267 | def channels (self): 268 | """ 269 | Список открытых каналов. 270 | """ 271 | 272 | return json.loads(self._get ('/channels')) 273 | 274 | def exchanges (self): 275 | """ 276 | Список exchange. 277 | 278 | *Example:*\n 279 | | ${exchanges}= | Exchanges | 280 | | Log List | ${exchanges} | 281 | | ${item}= | Get From list | ${exchanges} | 1 | 282 | | ${name}= | Get From Dictionary | ${q} | name | 283 | =>\n 284 | List length is 8 and it contains following items: 285 | | 0 | {u'name': u'', u'durable': True, u'vhost': u'/', u'internal': False, u'message_stats': [], u'arguments': {}, u'type': u'direct', u'auto_delete': False} | 286 | | 1 | {u'name': u'amq.direct', u'durable': True, u'vhost': u'/', u'internal': False, u'message_stats': [], u'arguments': {}, u'type': u'direct', u'auto_delete': False} | 287 | ...\n 288 | ${name} = amq.direct 289 | """ 290 | 291 | return json.loads(self._get ('/exchanges')) 292 | 293 | def get_names_of_all_exchanges (self): 294 | """ 295 | Список имён всех exchanges. 296 | 297 | *Example:*\n 298 | | ${names}= | Get Names Of All Exchanges | 299 | | Log List | ${names} | 300 | =>\n 301 | | List has one item: 302 | | amq.direct 303 | """ 304 | 305 | names=[] 306 | data=self.exchanges () 307 | for item in data : 308 | names.append(item['name']) 309 | return names 310 | 311 | def queues (self): 312 | """ 313 | Список очередей. 314 | """ 315 | 316 | return json.loads(self._get ('/queues')) 317 | 318 | def get_queues_on_vhost (self, vhost = '%2F'): 319 | """ 320 | Список очередей для виртуального хоста. 321 | 322 | *Args:*\n 323 | _vhost_ -имя виртуального хоста (перекодируется при помощи urllib.quote); 324 | """ 325 | 326 | return json.loads(self._get ('/queues/'+self._quote_vhost(vhost))) 327 | 328 | def get_names_of_queues_on_vhost (self, vhost = '%2F'): 329 | """ 330 | Список имен очередей виртуального хоста. 331 | 332 | *Args:*\n 333 | - vhost: имя виртуального хоста (перекодируется при помощи urllib.quote) 334 | 335 | *Example:*\n 336 | | ${names}= | Get Names Of Queues On Vhost | 337 | | Log List | ${names} | 338 | =>\n 339 | | List has one item: 340 | | federation: ex2 -> rabbit@server.net.ru 341 | """ 342 | names=[] 343 | data=self.get_queues_on_vhost (vhost) 344 | for item in data : 345 | names.append(item['name']) 346 | return names 347 | 348 | def queue_exists(self, queue, vhost='%2F'): 349 | """ 350 | Verifies that the one or more queues exists 351 | """ 352 | names = self.get_names_of_queues_on_vhost() 353 | if queue in names: 354 | return True 355 | else: 356 | return False 357 | 358 | def delete_queues_by_name (self, name, vhost = '%2F'): 359 | """ 360 | Удаление очереди с виртуального хоста. 361 | 362 | *Args:*\n 363 | _name_ - имя очереди (перекодируется urllib.quote);\n 364 | _vhost_ - имя виртуального хоста (перекодируется urllib.quote);\n 365 | """ 366 | 367 | return self._delete('/queues/'+self._quote_vhost(vhost)+'/'+urllib.quote(name)) 368 | 369 | def vhosts (self): 370 | """ 371 | Список виртуальных хостов. 372 | """ 373 | 374 | return json.loads(self._get ('/vhosts')) 375 | 376 | 377 | def nodes(self): 378 | """ 379 | List of nodes in the RabbitMQ cluster 380 | """ 381 | return json.loads(self._get('/nodes')) 382 | 383 | @property 384 | def _cluster_name(self): 385 | """ 386 | Name identifying this RabbitMQ cluster. 387 | """ 388 | return json.loads(self._get('/cluster-name')) 389 | 390 | def create_queues_by_name(self, name, auto_delete=False, durable=True, arguments={}, vhost='%2F'): 391 | """ 392 | Create an individual queue. 393 | """ 394 | node = self._cluster_name['name'] 395 | body = json.dumps({ 396 | "auto_delete": auto_delete, 397 | "durable": durable, 398 | "arguments": arguments, 399 | "node": node 400 | }) 401 | return self._put('/queues/' + self._quote_vhost(vhost) + '/' + urllib.quote(name), body=body) 402 | 403 | def publish_message_by_name(self, queue, msg, properties, vhost='%2F'): 404 | """ 405 | Publish a message to a given exchange 406 | """ 407 | 408 | name = "amq.default" 409 | body = json.dumps({ 410 | "properties": properties, 411 | "routing_key": queue, 412 | "payload": msg, 413 | "payload_encoding": "string" 414 | }) 415 | routed = self._post('/exchanges/' + self._quote_vhost(vhost) + 416 | '/' + urllib.quote(name) + '/publish', body=body) 417 | return json.loads(routed) 418 | 419 | def get_messages_by_queue(self, queue, count=5, requeue=False, encoding="auto", truncate=50000, vhost='%2F'): 420 | """ 421 | Get messages from a queue. 422 | """ 423 | 424 | body = json.dumps({ 425 | "count": count, 426 | "requeue": requeue, 427 | "encoding": encoding, 428 | "truncate": truncate 429 | }) 430 | messages = self._post('/queues/' + self._quote_vhost(vhost) + 431 | '/' + urllib.quote(queue) + '/get', body=body) 432 | return json.loads(messages) 433 | 434 | def purge_messages_by_queue(self, name, vhost='%2F'): 435 | """ 436 | Purge contents of a queue. 437 | """ 438 | return self._delete('/queues/' + self._quote_vhost(vhost) + '/' + urllib.quote(name) + '/contents') 439 | 440 | -------------------------------------------------------------------------------- /doc/AdvancedLogging.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 82 | 92 | 115 | 155 | 159 | 171 | 174 | 175 | 176 | 177 | 178 |
179 |

Opening library documentation failed

180 |
    181 |
  • Verify that you have JavaScript enabled in your browser.
  • 182 |
  • Make sure you are using a modern enough browser. Firefox 3.5, IE 8, or equivalent is required, newer browsers are recommended.
  • 183 |
  • Check are there messages in your browser's JavaScript error log. Please report the problem if you suspect you have encountered a bug.
  • 184 |
185 |
186 | 187 | 191 | 192 | 217 | 218 | 228 | 229 | 244 | 245 | 254 | 255 | 272 | 279 | 280 | 281 | 282 | --------------------------------------------------------------------------------