class OracleDB(object): """ Библиотека для работы с базой данных Oracle. == Зависимости == | cx_Oracle | http://cx-oracle.sourceforge.net | version > 3.0 | | robot framework | http://robotframework.org | """ ROBOT_LIBRARY_SCOPE = 'GLOBAL' def __init__(self): self._connection = None self._cache = ConnectionCache() # использование кеша Robot Framework для одновременной работы с несколькими соединениями def connect_to_oracle (self, dbName, dbUserName, dbPassword, alias=None): """ Подключение к Oracle. *Args:*\n _dbName_ - имя базы данных;\n _dbUserName_ - имя пользователя;\n _dbPassword_ - пароль пользователя;\n _alias_ - псевдоним соединения;\n *Returns:*\n Индекс текущего соединения. *Example:*\n | Connect To Oracle | rb60db | bis | password | """ try: logger.debug ('Connecting using : dbName=%s, dbUserName=%s, dbPassword=%s ' % (dbName, dbUserName, dbPassword)) connection_string = '%s/%s@%s' % (dbUserName,dbPassword,dbName) self._connection=cx_Oracle.connect(connection_string) return self._cache.register(self._connection, alias) except cx_Oracle.DatabaseError,info: raise Exception ("Logon to oracle Error:",str(info))
class TestConnectionCache(unittest.TestCase): def setUp(self): self.cache = ConnectionCache() def test_initial(self): self._verify_initial_state() def test_no_connection(self): assert_raises_with_msg(RuntimeError, 'No open connection.', getattr, ConnectionCache().current, 'whatever') assert_raises_with_msg(RuntimeError, 'Custom msg', getattr, ConnectionCache('Custom msg').current, 'xxx') def test_register_one(self): conn = ConnectionMock() index = self.cache.register(conn) assert_equal(index, 1) assert_equal(self.cache.current, conn) assert_equal(self.cache.current_index, 1) assert_equal(self.cache._connections, [conn]) assert_equal(self.cache._aliases, {}) def test_register_multiple(self): conns = [ConnectionMock(1), ConnectionMock(2), ConnectionMock(3)] for i, conn in enumerate(conns): index = self.cache.register(conn) assert_equal(index, i+1) assert_equal(self.cache.current, conn) assert_equal(self.cache.current_index, i+1) assert_equal(self.cache._connections, conns) def test_register_multiple_equal_objects(self): conns = [ConnectionMock(1), ConnectionMock(1), ConnectionMock(1)] for i, conn in enumerate(conns): index = self.cache.register(conn) assert_equal(index, i+1) assert_equal(self.cache.current, conn) assert_equal(self.cache.current_index, i+1) assert_equal(self.cache._connections, conns) def test_register_multiple_same_object(self): conns = [ConnectionMock()] * 3 for i, conn in enumerate(conns): index = self.cache.register(conn) assert_equal(index, i+1) assert_equal(self.cache.current, conn) assert_equal(self.cache.current_index, 1) assert_equal(self.cache._connections, conns) def test_set_current_index(self): self.cache.current_index = None assert_equal(self.cache.current_index, None) self._register('a', 'b') self.cache.current_index = 1 assert_equal(self.cache.current_index, 1) assert_equal(self.cache.current.id, 'a') self.cache.current_index = None assert_equal(self.cache.current_index, None) assert_equal(self.cache.current, self.cache._no_current) self.cache.current_index = 2 assert_equal(self.cache.current_index, 2) assert_equal(self.cache.current.id, 'b') def test_set_invalid_index(self): assert_raises(IndexError, setattr, self.cache, 'current_index', 1) def test_switch_with_index(self): self._register('a', 'b', 'c') self._assert_current('c', 3) self.cache.switch(1) self._assert_current('a', 1) self.cache.switch('2') self._assert_current('b', 2) def _assert_current(self, id, index): assert_equal(self.cache.current.id, id) assert_equal(self.cache.current_index, index) def test_switch_with_non_existing_index(self): self._register('a', 'b') assert_raises_with_msg(RuntimeError, "Non-existing index or alias '3'.", self.cache.switch, 3) assert_raises_with_msg(RuntimeError, "Non-existing index or alias '42'.", self.cache.switch, 42) def test_register_with_alias(self): conn = ConnectionMock() index = self.cache.register(conn, 'My Connection') assert_equal(index, 1) assert_equal(self.cache.current, conn) assert_equal(self.cache._connections, [conn]) assert_equal(self.cache._aliases, {'myconnection': 1}) def test_register_multiple_with_alias(self): c1 = ConnectionMock(); c2 = ConnectionMock(); c3 = ConnectionMock() for i, conn in enumerate([c1,c2,c3]): index = self.cache.register(conn, 'c%d' % (i+1)) assert_equal(index, i+1) assert_equal(self.cache.current, conn) assert_equal(self.cache._connections, [c1, c2, c3]) assert_equal(self.cache._aliases, {'c1': 1, 'c2': 2, 'c3': 3}) def test_switch_with_alias(self): self._register('a', 'b', 'c', 'd', 'e') assert_equal(self.cache.current.id, 'e') self.cache.switch('a') assert_equal(self.cache.current.id, 'a') self.cache.switch('C') assert_equal(self.cache.current.id, 'c') self.cache.switch(' B ') assert_equal(self.cache.current.id, 'b') def test_switch_with_non_existing_alias(self): self._register('a', 'b') assert_raises_with_msg(RuntimeError, "Non-existing index or alias 'whatever'.", self.cache.switch, 'whatever') def test_switch_with_alias_overriding_index(self): self._register('2', '1') self.cache.switch(1) assert_equal(self.cache.current.id, '2') self.cache.switch('1') assert_equal(self.cache.current.id, '1') def test_get_connection_with_index(self): self._register('a', 'b') assert_equal(self.cache.get_connection(1).id, 'a') assert_equal(self.cache.current.id, 'b') assert_equal(self.cache[2].id, 'b') def test_get_connection_with_alias(self): self._register('a', 'b') assert_equal(self.cache.get_connection('a').id, 'a') assert_equal(self.cache.current.id, 'b') assert_equal(self.cache['b'].id, 'b') def test_get_connection_with_none_returns_current(self): self._register('a', 'b') assert_equal(self.cache.get_connection().id, 'b') assert_equal(self.cache[None].id, 'b') def test_get_connection_with_none_fails_if_no_current(self): assert_raises_with_msg(RuntimeError, 'No open connection.', self.cache.get_connection) def test_close_all(self): connections = self._register('a', 'b', 'c', 'd') self.cache.close_all() self._verify_initial_state() for conn in connections: assert_true(conn.closed_by_close) def test_close_all_with_given_method(self): connections = self._register('a', 'b', 'c', 'd') self.cache.close_all('exit') self._verify_initial_state() for conn in connections: assert_true(conn.closed_by_exit) def test_empty_cache(self): connections = self._register('a', 'b', 'c', 'd') self.cache.empty_cache() self._verify_initial_state() for conn in connections: assert_false(conn.closed_by_close) assert_false(conn.closed_by_exit) def test_iter(self): conns = ['a', object(), 1, None] for c in conns: self.cache.register(c) assert_equal(list(self.cache), conns) def test_len(self): assert_equal(len(self.cache), 0) self.cache.register(None) assert_equal(len(self.cache), 1) self.cache.register(None) assert_equal(len(self.cache), 2) self.cache.empty_cache() assert_equal(len(self.cache), 0) def test_truthy(self): assert_false(self.cache) self.cache.register(None) assert_true(self.cache) self.cache.current_index = None assert_false(self.cache) self.cache.current_index = 1 assert_true(self.cache) self.cache.empty_cache() assert_false(self.cache) def test_resolve_alias_or_index(self): self.cache.register(ConnectionMock(), 'alias') assert_equal(self.cache._resolve_alias_or_index('alias'), 1) assert_equal(self.cache.resolve_alias_or_index('1'), 1) assert_equal(self.cache.resolve_alias_or_index(1), 1) def test_resolve_invalid_alias_or_index(self): assert_raises_with_msg(ValueError, "Non-existing index or alias 'nonex'.", self.cache._resolve_alias_or_index, 'nonex') assert_raises_with_msg(ValueError, "Non-existing index or alias '1'.", self.cache.resolve_alias_or_index, '1') assert_raises_with_msg(ValueError, "Non-existing index or alias '42'.", self.cache.resolve_alias_or_index, 42) def _verify_initial_state(self): assert_equal(self.cache.current, self.cache._no_current) assert_equal(self.cache.current_index, None) assert_equal(self.cache._connections, []) assert_equal(self.cache._aliases, {}) def _register(self, *ids): connections = [] for id in ids: conn = ConnectionMock(id) self.cache.register(conn, id) connections.append(conn) return connections
class BaseAppLibrary(object): ROBOT_LIBRARY_SCOPE = 'GLOBAL' if in_robot_context: __metaclass__ = _StateCapturing def __init__(self): self._cache = ConnectionCache() def open_session(self, device_id, alias=None): """Open a session. ``device_id`` is an identifier for looking up configurations of a specific device. The optional ``alias`` provided here can be used to switch between sessions/devices later. See `Switch Device` for more details. """ _logger.info('Open session; device ID = [%s], alias = [%s])', device_id, alias) # init context and install delegates context = self._init_context(device_id) context._log_screenshot_delegate = self._log_screenshot_delegate context._log_page_source_delegate = self._log_page_source_delegate self._cache.register(RFConnectionCache(context), alias) def open_app(self, reset=None): """Open the app. To reset app state prior to opening the app, pass a non-empty string to ``reset`` argument. Examples: | Open App | reset | # reset app state | | Open App | | # do not reset app state | """ context = self._current_context context.open_app(bool(reset)) context.logs_all = [] # accumulate logs of each step log_text('\n'.join(context.get_initial_logs()), 'APP LOGS (Initial)', 'app_logs_initial_', '.log') def _init_context(self): raise NotImplementedError() def _log_screenshot_delegate(self, msg, *args, **kwargs): msg = msg % args level = kwargs['level'] if 'level' in kwargs else logging.INFO page = kwargs['page'] if 'page' in kwargs else None if page: msg += ' (%s)' % page.__class__.__name__ log_screenshot(self._current_context.take_screenshot_as_png(), msg, level=level) def _log_page_source_delegate(self, msg, *args, **kwargs): msg = msg % args level = kwargs['level'] if 'level' in kwargs else logging.INFO page = kwargs['page'] if 'page' in kwargs else None if page: msg += ' (%s)' % page.__class__.__name__ source, ext = self._current_context.dump_page_source() log_text(source, msg, prefix='page_source_', suffix='.%s' % ext, level=level) def close_session(self): """Terminate current session.""" self._cache.current.close() def close_all_sessions(self): """Terminate all open sessions.""" self._cache.close_all() def close_app(self): """Close the app.""" self._cache.current.close_app() def switch_device(self, alias): """Switch between sessions/devices using alias. Examples: | Open App | A | # current session/device is A | | Open App | B | # current session/device becomes B | | Switch Device | A | # switch back to A | """ self._cache.switch(alias) def _capture_state(self, after=False, err=None): # To increase efficiency, screenshots are no longer taken automatically. # Developers should explicitly do that AFTER the UI has been changed. if not after: return context = self._current_context try: if after: logs_step = context.get_new_logs() context.logs_all.extend(logs_step) log_text('\n'.join(logs_step), 'APP LOGS (Step)', 'app_logs_step_', '.log') except: _logger.warning('Fail to capture state.', exc_info=True) @property def _current_context(self): return self._cache.current._context @property def _current_page(self): return self._cache.current._context.current_page @_current_page.setter def _current_page(self, page): self._cache.current._context.current_page = page
class RabbitMqManager(object): """ Библиотека для управления сервером RabbitMq. Реализована на основе: - [ http://hg.rabbitmq.com/rabbitmq-management/raw-file/3646dee55e02/priv/www-api/help.html | RabbitMQ Management HTTP API ] - [ https://github.com/rabbitmq/rabbitmq-management/blob/master/bin/rabbitmqadmin | rabbitmqadmin ] == Зависимости == | robot framework | http://robotframework.org | == Example == | *Settings* | *Value* | | Library | RabbitMqManager | | Library | Collections | | *Test Cases* | *Action* | *Argument* | *Argument* | *Argument* | *Argument* | *Argument* | | Simple | | | Connect To Rabbitmq | my_host_name | 15672 | guest | guest | alias=rmq | | | ${overview}= | Overview | | | Log Dictionary | ${overview} | | | Close All Rabbitmq Connections | """ ROBOT_LIBRARY_SCOPE='GLOBAL' def __init__(self): self._connection=None self.headers=None self._cache=ConnectionCache() def connect_to_rabbitmq (self, host, port, username = '******', password = '******', timeout = 15, alias = None): """ Подключение к серверу RabbitMq. *Args:*\n _host_ - имя сервера;\n _port_ - номер порта;\n _username_ - имя пользователя;\n _password_ - пароль пользователя;\n _timeout_ - время ожидания соединения;\n _alias_ - псевдоним соединения;\n *Returns:*\n Индекс текущего соединения. *Raises:*\n socket.error в том случае, если невозможно создать соединение. *Example:*\n | Connect To Rabbitmq | my_host_name | 15672 | guest | guest | alias=rmq | """ port=int (port) timeout=int (timeout) logger.debug ('Connecting using : host=%s, port=%d, username=%s, password=%s, timeout=%d, alias=%s '%(host, port, username, password, timeout, alias)) self.headers={"Authorization":"Basic "+base64.b64encode(username+":"+password)} try: self._connection=httplib.HTTPConnection (host, port, timeout) self._connection.connect() return self._cache.register(self._connection, alias) except socket.error, e: raise Exception ("Could not connect to RabbitMq", str(e))
class Process(object): """Robot Framework test library for running processes. This library utilizes Python's [http://docs.python.org/2/library/subprocess.html|subprocess] module and its [http://docs.python.org/2/library/subprocess.html#subprocess.Popen|Popen] class. The library has following main usages: - Running processes in system and waiting for their completion using `Run Process` keyword. - Starting processes on background using `Start Process`. - Waiting started process to complete using `Wait For Process` or stopping them with `Terminate Process` or `Terminate All Processes`. This library is new in Robot Framework 2.8. == Table of contents == - `Specifying command and arguments` - `Process configuration` - `Active process` - `Result object` - `Boolean arguments` - `Using with OperatingSystem library` - `Example` - `Shortcuts` - `Keywords` = Specifying command and arguments = Both `Run Process` and `Start Process` accept the command to execute and all arguments passed to it as separate arguments. This is convenient to use and also allows these keywords to automatically escape possible spaces and other special characters in the command or arguments. When `running processes in shell`, it is also possible to give the whole command to execute as a single string. The command can then contain multiple commands, for example, connected with pipes. When using this approach the caller is responsible on escaping. Examples: | `Run Process` | ${progdir}${/}prog.py | first arg | second | | `Run Process` | script1.sh arg && script2.sh | shell=yes | cwd=${progdir} | Starting from Robot Framework 2.8.6, possible non-string arguments are converted to strings automatically. = Process configuration = `Run Process` and `Start Process` keywords can be configured using optional `**configuration` keyword arguments. Configuration arguments must be given after other arguments passed to these keywords and must use syntax like `name=value`. Available configuration arguments are listed below and discussed further in sections afterwards. | = Name = | = Explanation = | | shell | Specifies whether to run the command in shell or not | | cwd | Specifies the working directory. | | env | Specifies environment variables given to the process. | | env:<name> | Overrides the named environment variable(s) only. | | stdout | Path of a file where to write standard output. | | stderr | Path of a file where to write standard error. | | alias | Alias given to the process. | Note that because `**configuration` is passed using `name=value` syntax, possible equal signs in other arguments passed to `Run Process` and `Start Process` must be escaped with a backslash like `name\\=value`. See `Run Process` for an example. == Running processes in shell == The `shell` argument specifies whether to run the process in a shell or not. By default shell is not used, which means that shell specific commands, like `copy` and `dir` on Windows, are not available. Giving the `shell` argument any non-false value, such as `shell=True`, changes the program to be executed in a shell. It allows using the shell capabilities, but can also make the process invocation operating system dependent. When using a shell it is possible to give the whole command to execute as a single string. See `Specifying command and arguments` section for examples and more details in general. == Current working directory == By default the child process will be executed in the same directory as the parent process, the process running tests, is executed. This can be changed by giving an alternative location using the `cwd` argument. Forward slashes in the given path are automatically converted to backslashes on Windows. `Standard output and error streams`, when redirected to files, are also relative to the current working directory possibly set using the `cwd` argument. Example: | `Run Process` | prog.exe | cwd=${ROOT}/directory | stdout=stdout.txt | == Environment variables == By default the child process will get a copy of the parent process's environment variables. The `env` argument can be used to give the child a custom environment as a Python dictionary. If there is a need to specify only certain environment variable, it is possible to use the `env:<name>=<value>` format to set or override only that named variables. It is also possible to use these two approaches together. Examples: | `Run Process` | program | env=${environ} | | `Run Process` | program | env:http_proxy=10.144.1.10:8080 | env:PATH=%{PATH}${:}${PROGDIR} | | `Run Process` | program | env=${environ} | env:EXTRA=value | == Standard output and error streams == By default processes are run so that their standard output and standard error streams are kept in the memory. This works fine normally, but if there is a lot of output, the output buffers may get full and the program can hang. To avoid output buffers getting full, it is possible to use `stdout` and `stderr` arguments to specify files on the file system where to redirect the outputs. This can also be useful if other processes or other keywords need to read or manipulate the outputs somehow. Given `stdout` and `stderr` paths are relative to the `current working directory`. Forward slashes in the given paths are automatically converted to backslashes on Windows. As a special feature, it is possible to redirect the standard error to the standard output by using `stderr=STDOUT`. Regardless are outputs redirected to files or not, they are accessible through the `result object` returned when the process ends. Examples: | ${result} = | `Run Process` | program | stdout=${TEMPDIR}/stdout.txt | stderr=${TEMPDIR}/stderr.txt | | `Log Many` | stdout: ${result.stdout} | stderr: ${result.stderr} | | ${result} = | `Run Process` | program | stderr=STDOUT | | `Log` | all output: ${result.stdout} | Note that the created output files are not automatically removed after the test run. The user is responsible to remove them if needed. == Alias == A custom name given to the process that can be used when selecting the `active process`. Examples: | `Start Process` | program | alias=example | | `Run Process` | python | -c | print 'hello' | alias=hello | = Active process = The test library keeps record which of the started processes is currently active. By default it is latest process started with `Start Process`, but `Switch Process` can be used to select a different one. Using `Run Process` does not affect the active process. The keywords that operate on started processes will use the active process by default, but it is possible to explicitly select a different process using the `handle` argument. The handle can be the identifier returned by `Start Process` or an `alias` explicitly given to `Start Process` or `Run Process`. = Result object = `Run Process`, `Wait For Process` and `Terminate Process` keywords return a result object that contains information about the process execution as its attributes. The same result object, or some of its attributes, can also be get using `Get Process Result` keyword. Attributes available in the object are documented in the table below. | = Attribute = | = Explanation = | | rc | Return code of the process as an integer. | | stdout | Contents of the standard output stream. | | stderr | Contents of the standard error stream. | | stdout_path | Path where stdout was redirected or `None` if not redirected. | | stderr_path | Path where stderr was redirected or `None` if not redirected. | Example: | ${result} = | `Run Process` | program | | `Should Be Equal As Integers` | ${result.rc} | 0 | | `Should Match` | ${result.stdout} | Some t?xt* | | `Should Be Empty` | ${result.stderr} | | | ${stdout} = | `Get File` | ${result.stdout_path} | | `Should Be Equal` | ${stdout} | ${result.stdout} | | `File Should Be Empty` | ${result.stderr_path} | | = Boolean arguments = Some keywords accept arguments that are handled as Boolean values. If such an argument is given as a string, it is considered false if it is either empty or case-insensitively equal to `false`. Other strings are considered true regardless what they contain, and other argument types are tested using same [http://docs.python.org/2/library/stdtypes.html#truth-value-testing|rules as in Python]. True examples: | `Terminate Process` | kill=True | # Strings are generally true. | | `Terminate Process` | kill=yes | # Same as above. | | `Terminate Process` | kill=${TRUE} | # Python `True` is true. | | `Terminate Process` | kill=${42} | # Numbers other than 0 are true. | False examples: | `Terminate Process` | kill=False | # String `False` is false. | | `Terminate Process` | kill=${EMPTY} | # Empty string is false. | | `Terminate Process` | kill=${FALSE} | # Python `False` is false. | | `Terminate Process` | kill=${0} | # Number 0 is false. | Note that prior to Robot Framework 2.8 all non-empty strings, including `false`, were considered true. = Using with OperatingSystem library = The OperatingSystem library also contains keywords for running processes. They are not as flexible as the keywords provided by this library, and thus not recommended to be used anymore. They may eventually even be deprecated. There is a name collision because both of these libraries have `Start Process` and `Switch Process` keywords. This is handled so that if both libraries are imported, the keywords in the Process library are used by default. If there is a need to use the OperatingSystem variants, it is possible to use `OperatingSystem.Start Process` syntax or use the `BuiltIn` keyword `Set Library Search Order` to change the priority. Other keywords in the OperatingSystem library can be used freely with keywords in the Process library. = Example = | ***** Settings ***** | Library Process | Suite Teardown `Terminate All Processes` kill=True | | ***** Test Cases ***** | Example | `Start Process` program arg1 arg2 alias=First | ${handle} = `Start Process` command.sh arg | command2.sh shell=True cwd=/path | ${result} = `Run Process` ${CURDIR}/script.py | `Should Not Contain` ${result.stdout} FAIL | `Terminate Process` ${handle} | ${result} = `Wait For Process` First | `Should Be Equal As Integers` ${result.rc} 0 """ ROBOT_LIBRARY_SCOPE = 'GLOBAL' ROBOT_LIBRARY_VERSION = get_version() TERMINATE_TIMEOUT = 30 KILL_TIMEOUT = 10 def __init__(self): self._processes = ConnectionCache('No active process.') self._results = {} def run_process(self, command, *arguments, **configuration): """Runs a process and waits for it to complete. `command` and `*arguments` specify the command to execute and arguments passed to it. See `Specifying command and arguments` for more details. `**configuration` contains additional configuration related to starting processes and waiting for them to finish. See `Process configuration` for more details about configuration related to starting processes. Configuration related to waiting for processes consists of `timeout` and `on_timeout` arguments that have same semantics as with `Wait For Process` keyword. By default there is no timeout, and if timeout is defined the default action on timeout is `terminate`. Returns a `result object` containing information about the execution. Note that possible equal signs in `*arguments` must be escaped with a backslash (e.g. `name\\=value`) to avoid them to be passed in as `**configuration`. Examples: | ${result} = | Run Process | python | -c | print 'Hello, world!' | | Should Be Equal | ${result.stdout} | Hello, world! | | ${result} = | Run Process | ${command} | stderr=STDOUT | timeout=10s | | ${result} = | Run Process | ${command} | timeout=1min | on_timeout=continue | | ${result} = | Run Process | java -Dname\\=value Example | shell=True | cwd=${EXAMPLE} | This command does not change the `active process`. `timeout` and `on_timeout` arguments are new in Robot Framework 2.8.4. """ current = self._processes.current timeout = configuration.pop('timeout', None) on_timeout = configuration.pop('on_timeout', 'terminate') try: handle = self.start_process(command, *arguments, **configuration) return self.wait_for_process(handle, timeout, on_timeout) finally: self._processes.current = current def start_process(self, command, *arguments, **configuration): """Starts a new process on background. See `Specifying command and arguments` and `Process configuration` for more information about the arguments, and `Run Process` keyword for related examples. Makes the started process new `active process`. Returns an identifier that can be used as a handle to active the started process if needed. Starting from Robot Framework 2.8.5, processes are started so that they create a new process group. This allows sending signals to and terminating also possible child processes. """ config = ProcessConfig(**configuration) executable_command = self._cmd(command, arguments, config.shell) logger.info('Starting process:\n%s' % executable_command) logger.debug('Process configuration:\n%s' % config) process = subprocess.Popen(executable_command, **config.full_config) self._results[process] = ExecutionResult(process, config.stdout_stream, config.stderr_stream) return self._processes.register(process, alias=config.alias) def _cmd(self, command, args, use_shell): command = [encode_to_system(item) for item in [command] + list(args)] if not use_shell: return command if args: return subprocess.list2cmdline(command) return command[0] def is_process_running(self, handle=None): """Checks is the process running or not. If `handle` is not given, uses the current `active process`. Returns `True` if the process is still running and `False` otherwise. """ return self._processes[handle].poll() is None def process_should_be_running(self, handle=None, error_message='Process is not running.'): """Verifies that the process is running. If `handle` is not given, uses the current `active process`. Fails if the process has stopped. """ if not self.is_process_running(handle): raise AssertionError(error_message) def process_should_be_stopped(self, handle=None, error_message='Process is running.'): """Verifies that the process is not running. If `handle` is not given, uses the current `active process`. Fails if the process is still running. """ if self.is_process_running(handle): raise AssertionError(error_message) def wait_for_process(self, handle=None, timeout=None, on_timeout='continue'): """Waits for the process to complete or to reach the given timeout. The process to wait for must have been started earlier with `Start Process`. If `handle` is not given, uses the current `active process`. `timeout` defines the maximum time to wait for the process. It is interpreted according to Robot Framework User Guide Appendix `Time Format`, for example, '42', '42 s', or '1 minute 30 seconds'. `on_timeout` defines what to do if the timeout occurs. Possible values and corresponding actions are explained in the table below. Notice that reaching the timeout never fails the test. | = Value = | = Action = | | `continue` | The process is left running (default). | | `terminate` | The process is gracefully terminated. | | `kill` | The process is forcefully stopped. | See `Terminate Process` keyword for more details how processes are terminated and killed. If the process ends before the timeout or it is terminated or killed, this keyword returns a `result object` containing information about the execution. If the process is left running, Python `None` is returned instead. Examples: | # Process ends cleanly | | | | ${result} = | Wait For Process | example | | Process Should Be Stopped | example | | | Should Be Equal As Integers | ${result.rc} | 0 | | # Process does not end | | | | ${result} = | Wait For Process | timeout=42 secs | | Process Should Be Running | | | | Should Be Equal | ${result} | ${NONE} | | # Kill non-ending process | | | | ${result} = | Wait For Process | timeout=1min 30s | on_timeout=kill | | Process Should Be Stopped | | | | Should Be Equal As Integers | ${result.rc} | -9 | `timeout` and `on_timeout` are new in Robot Framework 2.8.2. """ process = self._processes[handle] logger.info('Waiting for process to complete.') if timeout: timeout = timestr_to_secs(timeout) if not self._process_is_stopped(process, timeout): logger.info('Process did not complete in %s.' % secs_to_timestr(timeout)) return self._manage_process_timeout(handle, on_timeout.lower()) return self._wait(process) def _manage_process_timeout(self, handle, on_timeout): if on_timeout == 'terminate': return self.terminate_process(handle) elif on_timeout == 'kill': return self.terminate_process(handle, kill=True) else: logger.info('Leaving process intact.') return None def _wait(self, process): result = self._results[process] result.rc = process.wait() or 0 result.close_streams() logger.info('Process completed.') return result def terminate_process(self, handle=None, kill=False): """Stops the process gracefully or forcefully. If `handle` is not given, uses the current `active process`. Waits for the process to stop after terminating it. Returns a `result object` containing information about the execution similarly as `Wait For Process`. On Unix-like machines, by default, first tries to terminate the process group gracefully, but forcefully kills it if it does not stop in 30 seconds. Kills the process group immediately if the `kill` argument is given any value considered true. See `Boolean arguments` section for more details about true and false values. Termination is done using `TERM (15)` signal and killing using `KILL (9)`. Use `Send Signal To Process` instead if you just want to send either of these signals without waiting for the process to stop. On Windows, by default, sends `CTRL_BREAK_EVENT` signal to the process group. If that does not stop the process in 30 seconds, or `kill` argument is given a true value, uses Win32 API function `TerminateProcess()` to kill the process forcefully. Note that `TerminateProcess()` does not kill possible child processes. | ${result} = | Terminate Process | | | Should Be Equal As Integers | ${result.rc} | -15 | # On Unixes | | Terminate Process | myproc | kill=true | *NOTE:* Stopping processes requires the [http://docs.python.org/2/library/subprocess.html|subprocess] module to have working `terminate` and `kill` functions. They were added in Python 2.6 and are thus missing from earlier versions. Unfortunately at least beta releases of Jython 2.7 [http://bugs.jython.org/issue1898|do not seem to support them either]. Automatically killing the process if termination fails as well as returning a result object are new features in Robot Framework 2.8.2. Terminating also possible child processes, including using `CTRL_BREAK_EVENT` on Windows, is new in Robot Framework 2.8.5. """ process = self._processes[handle] if not hasattr(process, 'terminate'): raise RuntimeError('Terminating processes is not supported ' 'by this Python version.') terminator = self._kill if is_true(kill) else self._terminate try: terminator(process) except OSError: if not self._process_is_stopped(process, self.KILL_TIMEOUT): raise logger.debug('Ignored OSError because process was stopped.') return self._wait(process) def _kill(self, process): logger.info('Forcefully killing process.') if hasattr(os, 'killpg'): os.killpg(process.pid, signal_module.SIGKILL) else: process.kill() if not self._process_is_stopped(process, self.KILL_TIMEOUT): raise RuntimeError('Failed to kill process.') def _terminate(self, process): logger.info('Gracefully terminating process.') # Sends signal to the whole process group both on POSIX and on Windows # if supported by the interpreter. if hasattr(os, 'killpg'): os.killpg(process.pid, signal_module.SIGTERM) elif hasattr(signal_module, 'CTRL_BREAK_EVENT'): if sys.platform == 'cli': # https://ironpython.codeplex.com/workitem/35020 ctypes.windll.kernel32.GenerateConsoleCtrlEvent( signal_module.CTRL_BREAK_EVENT, process.pid) else: process.send_signal(signal_module.CTRL_BREAK_EVENT) else: process.terminate() if not self._process_is_stopped(process, self.TERMINATE_TIMEOUT): logger.info('Graceful termination failed.') self._kill(process) def terminate_all_processes(self, kill=False): """Terminates all still running processes started by this library. This keyword can be used in suite teardown or elsewhere to make sure that all processes are stopped, By default tries to terminate processes gracefully, but can be configured to forcefully kill them immediately. See `Terminate Process` that this keyword uses internally for more details. """ for handle in range(1, len(self._processes) + 1): if self.is_process_running(handle): self.terminate_process(handle, kill=kill) self.__init__() def send_signal_to_process(self, signal, handle=None, group=False): """Sends the given `signal` to the specified process. If `handle` is not given, uses the current `active process`. Signal can be specified either as an integer, or anything that can be converted to an integer, or as a signal name. In the latter case it is possible to give the name both with or without a `SIG` prefix, but names are case-sensitive. For example, all the examples below send signal `INT (2)`: | Send Signal To Process | 2 | | # Send to active process | | Send Signal To Process | INT | | | | Send Signal To Process | SIGINT | myproc | # Send to named process | What signals are supported depends on the system. For a list of existing signals on your system, see the Unix man pages related to signal handling (typically `man signal` or `man 7 signal`). By default sends the signal only to the parent process, not to possible child processes started by it. Notice that when `running processes in shell`, the shell is the parent process and it depends on the system does the shell propagate the signal to the actual started process. To send the signal to the whole process group, `group` argument can be set to any true value: | Send Signal To Process | TERM | group=yes | If you are stopping a process, it is often easier and safer to use `Terminate Process` keyword instead. *NOTE:* Sending signals requires the [http://docs.python.org/2/library/subprocess.html|subprocess] module to have working `send_signal` function. It was added in Python 2.6 and are thus missing from earlier versions. How well it will work with forthcoming Jython 2.7 is unknown. New in Robot Framework 2.8.2. Support for `group` argument is new in Robot Framework 2.8.5. """ if os.sep == '\\': raise RuntimeError('This keyword does not work on Windows.') process = self._processes[handle] signum = self._get_signal_number(signal) logger.info('Sending signal %s (%d).' % (signal, signum)) if is_true(group) and hasattr(os, 'killpg'): os.killpg(process.pid, signum) elif hasattr(process, 'send_signal'): process.send_signal(signum) else: raise RuntimeError('Sending signals is not supported ' 'by this Python version.') def _get_signal_number(self, int_or_name): try: return int(int_or_name) except ValueError: return self._convert_signal_name_to_number(int_or_name) def _convert_signal_name_to_number(self, name): try: return getattr(signal_module, name if name.startswith('SIG') else 'SIG' + name) except AttributeError: raise RuntimeError("Unsupported signal '%s'." % name) def get_process_id(self, handle=None): """Returns the process ID (pid) of the process. If `handle` is not given, uses the current `active process`. Returns the pid assigned by the operating system as an integer. Note that with Jython, at least with the 2.5 version, the returned pid seems to always be `None`. The pid is not the same as the identifier returned by `Start Process` that is used internally by this library. """ return self._processes[handle].pid def get_process_object(self, handle=None): """Return the underlying `subprocess.Popen` object. If `handle` is not given, uses the current `active process`. """ return self._processes[handle] def get_process_result(self, handle=None, rc=False, stdout=False, stderr=False, stdout_path=False, stderr_path=False): """Returns the specified `result object` or some of its attributes. The given `handle` specifies the process whose results should be returned. If no `handle` is given, results of the current `active process` are returned. In either case, the process must have been finishes before this keyword can be used. In practice this means that processes started with `Start Process` must be finished either with `Wait For Process` or `Terminate Process` before using this keyword. If no other arguments than the optional `handle` are given, a whole `result object` is returned. If one or more of the other arguments are given any true value, only the specified attributes of the `result object` are returned. These attributes are always returned in the same order as arguments are specified in the keyword signature. See `Boolean arguments` section for more details about true and false values. Examples: | Run Process | python | -c | print 'Hello, world!' | alias=myproc | | # Get result object | | | | ${result} = | Get Process Result | myproc | | Should Be Equal | ${result.rc} | ${0} | | Should Be Equal | ${result.stdout} | Hello, world! | | Should Be Empty | ${result.stderr} | | | # Get one attribute | | | | ${stdout} = | Get Process Result | myproc | stdout=true | | Should Be Equal | ${stdout} | Hello, world! | | # Multiple attributes | | | | ${stdout} | ${stderr} = | Get Process Result | myproc | stdout=yes | stderr=yes | | Should Be Equal | ${stdout} | Hello, world! | | Should Be Empty | ${stderr} | | Although getting results of a previously executed process can be handy in general, the main use case for this keyword is returning results over the remote library interface. The remote interface does not support returning the whole result object, but individual attributes can be returned without problems. New in Robot Framework 2.8.2. """ result = self._results[self._processes[handle]] if result.rc is None: raise RuntimeError('Getting results of unfinished processes ' 'is not supported.') attributes = self._get_result_attributes(result, rc, stdout, stderr, stdout_path, stderr_path) if not attributes: return result elif len(attributes) == 1: return attributes[0] return attributes def _get_result_attributes(self, result, *includes): attributes = (result.rc, result.stdout, result.stderr, result.stdout_path, result.stderr_path) includes = (is_true(incl) for incl in includes) return tuple(attr for attr, incl in zip(attributes, includes) if incl) def switch_process(self, handle): """Makes the specified process the current `active process`. The handle can be an identifier returned by `Start Process` or the `alias` given to it explicitly. Example: | Start Process | prog1 | alias=process1 | | Start Process | prog2 | alias=process2 | | # currently active process is process2 | | Switch Process | process1 | | # now active process is process1 | """ self._processes.switch(handle) def _process_is_stopped(self, process, timeout): max_time = time.time() + timeout while time.time() <= max_time: if process.poll() is not None: return True time.sleep(0.1) return False
class REST(object): def __init__(self): self._builtin = BuiltIn() self._cache = ConnectionCache() def create_rest_session(self, alias, headers=None, auth=None, verify=False, cert=None): """ Creates REST session with specified alias. Arguments: | alias | session alias | | headers | custom headers for all requests | | auth | basic auth | | verify | SSL verification | | cert | path to SSL certificate file | Example usage: | ${headers} | Create Dictionary | Content-Type | application/json | | @{service_basic_auth} | Set Variable | username | password | | Create Rest Session | session_alias | headers=${headers} | auth=${service_basic_auth} | verify=False | """ session = Session() if headers: session.headers.update(headers) if auth: session.auth = tuple(auth) session.verify = self._builtin.convert_to_boolean(verify) session.cert = cert self._cache.register(session, alias) def head(self, alias, url, params=None, headers=None, cookies=None, timeout=10): """ Sends HEAD request. Arguments: | alias | session alias | | url | service url | | params | request parameters | | headers | custom headers for request, rewrites session headers | | cookies | custom request cookies | | timeout | response timeout in seconds, raise exception on request timeout | Example usage: | ${payload} | Create Dictionary | param1 | value1 | param2 | value2 | | ${cookies} | Create Dictionary | sessionid | session12345 | | ${response} | Head | session_alias | http://localhost/service | params=${payload} | cookies=${cookies} | timeout=5 | """ logger.info("Sending HEAD request to: '%s', session: '%s'" % (url, alias)) session = self._cache.switch(alias) response = session.head(url, params=params, headers=headers, cookies=cookies, timeout=int(timeout)) return {"status": response.status_code, "headers": response.headers} def get(self, alias, url, params=None, headers=None, cookies=None, timeout=10): """ Sends GET request. See arguments description in `Head` keyword. """ logger.info("Sending GET request to: '%s', session: '%s'" % (url, alias)) session = self._cache.switch(alias) response = session.get(url, params=params, headers=headers, cookies=cookies, timeout=int(timeout)) try: return {"status": response.status_code, "headers": response.headers, "body": response.json()} except ValueError: return {"status": response.status_code, "headers": response.headers, "body": response.content} def post(self, alias, url, headers=None, cookies=None, data=None, files=None, timeout=10): """ Sends POST request. Arguments: | alias | session alias | | url | service url | | headers | custom headers for request, rewrites session headers | | cookies | custom request cookies | | data | dictionary, bytes, or file-like object to send in the body of the request | | files | dictionary of 'name': file-like-objects (or {'name': ('filename', fileobj)}) for multipart encoding upload | | timeout | response timeout in seconds, raise exception on request timeout | Example usage: | @{files} | Set Variable | path_to_file_1 | path_to_file_2 | | ${mpe_files} | Convert To Multipart Encoded Files | ${files} | | ${payload} | Set Variable | {"id": "34","doc_type": "history"} | | ${response} | Post | service_alias | http://localhost/service | data=${payload} | | ${response} | Post | service_alias | http://localhost/service | files=${mpe_files} | """ logger.info("Sending POST request to: '%s', session: '%s'" % (url, alias)) session = self._cache.switch(alias) response = session.post(url, headers=headers, cookies=cookies, data=data.encode("utf-8"), files=files, timeout=int(timeout)) try: return {"status": response.status_code, "headers": response.headers, "body": response.json()} except ValueError: return {"status": response.status_code, "headers": response.headers, "body": response.content} def put(self, alias, url, headers=None, data=None, cookies=None, timeout=10): """ Sends PUT request. See arguments description in `Post` keyword. """ logger.info("Sending PUT request to: '%s', session: '%s'" % (url, alias)) session = self._cache.switch(alias) response = session.put(url, headers=headers, cookies=cookies, data=data.encode("utf-8"), timeout=int(timeout)) try: return {"status": response.status_code, "headers": response.headers, "body": response.json()} except ValueError: return {"status": response.status_code, "headers": response.headers, "body": response.content} def delete(self, alias, url, headers=None, data=None, cookies=None, timeout=10): """ Sends DELETE request. See arguments description in `Post` keyword. """ logger.info("Sending DELETE request to: '%s', session: '%s'" % (url, alias)) session = self._cache.switch(alias) response = session.delete(url, headers=headers, cookies=cookies, data=data.encode("utf-8"), timeout=int(timeout)) try: return {"status": response.status_code, "headers": response.headers, "body": response.json()} except ValueError: return {"status": response.status_code, "headers": response.headers, "body": response.content} def close_all_sessions(self): """ Closes all created sessions. """ self._cache.empty_cache() @staticmethod def convert_to_multipart_encoded_files(files): """ Converts list of files to multipart encoded files. Example usage: | @{files} | Set Variable | path_to_file_1 | path_to_file_2 | | ${mpe_files} | Convert To Multipart Encoded Files | ${files} | """ mpe_files = [] for f in files: form_field_name = f[0] file_name = path.basename(f[1]) file_path = f[1] mime_type = f[2] mpe_files.append((form_field_name, (file_name, open(file_path, "rb"), mime_type))) return mpe_files
class TestConnnectionCache(unittest.TestCase): def setUp(self): self.cache = ConnectionCache() def test_initial(self): self._verify_initial_state() def test_no_connection(self): assert_raises_with_msg(RuntimeError, 'No open connection', getattr, ConnectionCache().current, 'whatever') assert_raises_with_msg(RuntimeError, 'Custom msg', getattr, ConnectionCache('Custom msg').current, 'xxx') def test_register_one(self): conn = ConnectionMock() index = self.cache.register(conn) assert_equals(index, 1) assert_equals(self.cache.current, conn) assert_equals(self.cache._connections, [conn]) assert_equals(self.cache._aliases, {}) def test_register_multiple(self): conns = [ConnectionMock(), ConnectionMock(), ConnectionMock()] for i, conn in enumerate(conns): index = self.cache.register(conn) assert_equals(index, i+1) assert_equals(self.cache.current, conn) assert_equals(self.cache._connections, conns) def test_switch_with_index(self): self._register('a', 'b', 'c') self._assert_current('c', 3) self.cache.switch(1) self._assert_current('a', 1) self.cache.switch('2') self._assert_current('b', 2) def _assert_current(self, id, index): assert_equals(self.cache.current.id, id) assert_equals(self.cache.current_index, index) def test_switch_with_non_existing_index(self): self._register('a', 'b') assert_raises_with_msg(RuntimeError, "Non-existing index or alias '3'", self.cache.switch, 3) assert_raises_with_msg(RuntimeError, "Non-existing index or alias '42'", self.cache.switch, 42) def test_register_with_alias(self): conn = ConnectionMock() index = self.cache.register(conn, 'My Connection') assert_equals(index, 1) assert_equals(self.cache.current, conn) assert_equals(self.cache._connections, [conn]) assert_equals(self.cache._aliases, {'myconnection': 1}) def test_register_multiple_with_alis(self): c1 = ConnectionMock(); c2 = ConnectionMock(); c3 = ConnectionMock() for i, conn in enumerate([c1,c2,c3]): index = self.cache.register(conn, 'c%d' % (i+1)) assert_equals(index, i+1) assert_equals(self.cache.current, conn) assert_equals(self.cache._connections, [c1, c2, c3]) assert_equals(self.cache._aliases, {'c1': 1, 'c2': 2, 'c3': 3}) def test_switch_with_alias(self): self._register('a', 'b', 'c', 'd', 'e') assert_equals(self.cache.current.id, 'e') self.cache.switch('a') assert_equals(self.cache.current.id, 'a') self.cache.switch('C') assert_equals(self.cache.current.id, 'c') self.cache.switch(' B ') assert_equals(self.cache.current.id, 'b') def test_switch_with_non_existing_alias(self): self._register('a', 'b') assert_raises_with_msg(RuntimeError, "Non-existing index or alias 'whatever'", self.cache.switch, 'whatever') def test_switch_with_alias_overriding_index(self): self._register('2', '1') self.cache.switch(1) assert_equals(self.cache.current.id, '2') self.cache.switch('1') assert_equals(self.cache.current.id, '1') def test_close_all(self): connections = self._register('a', 'b', 'c', 'd') self.cache.close_all() self._verify_initial_state() for conn in connections: assert_true(conn.closed_by_close) def test_close_all_with_given_method(self): connections = self._register('a', 'b', 'c', 'd') self.cache.close_all('exit') self._verify_initial_state() for conn in connections: assert_true(conn.closed_by_exit) def test_empty_cache(self): connections = self._register('a', 'b', 'c', 'd') self.cache.empty_cache() self._verify_initial_state() for conn in connections: assert_false(conn.closed_by_close) assert_false(conn.closed_by_exit) def _verify_initial_state(self): assert_equals(self.cache.current, self.cache._no_current) assert_none(self.cache.current_index) assert_equals(self.cache._connections, []) assert_equals(self.cache._aliases, {}) def _register(self, *ids): connections = [] for id in ids: conn = ConnectionMock(id) self.cache.register(conn, id) connections.append(conn) return connections
class Process(object): """Robot Framework test library for running processes. This library utilizes Python's [http://docs.python.org/2/library/subprocess.html|subprocess] module and its [http://docs.python.org/2/library/subprocess.html#subprocess.Popen|Popen] class. The library has following main usages: - Running processes in system and waiting for their completion using `Run Process` keyword. - Starting processes on background using `Start Process`. - Waiting started process to complete using `Wait For Process` or stopping them with `Terminate Process` or `Terminate All Processes`. This library is new in Robot Framework 2.8. == Table of contents == - `Specifying command and arguments` - `Process configuration` - `Active process` - `Result object` - `Boolean arguments` - `Example` - `Shortcuts` - `Keywords` = Specifying command and arguments = Both `Run Process` and `Start Process` accept the command to execute and all arguments passed to the command as separate arguments. This makes usage convenient and also allows these keywords to automatically escape possible spaces and other special characters in commands and arguments. Notice that if a command accepts options that themselves accept values, these options and their values must be given as separate arguments. When `running processes in shell`, it is also possible to give the whole command to execute as a single string. The command can then contain multiple commands to be run together. When using this approach, the caller is responsible on escaping. Examples: | `Run Process` | ${tools}${/}prog.py | argument | second arg with spaces | | `Run Process` | java | -jar | ${jars}${/}example.jar | --option | value | | `Run Process` | prog.py "one arg" && tool.sh | shell=yes | cwd=${tools} | Starting from Robot Framework 2.8.6, possible non-string arguments are converted to strings automatically. = Process configuration = `Run Process` and `Start Process` keywords can be configured using optional ``**configuration`` keyword arguments. Configuration arguments must be given after other arguments passed to these keywords and must use syntax like ``name=value``. Available configuration arguments are listed below and discussed further in sections afterwards. | = Name = | = Explanation = | | shell | Specifies whether to run the command in shell or not. | | cwd | Specifies the working directory. | | env | Specifies environment variables given to the process. | | env:<name> | Overrides the named environment variable(s) only. | | stdout | Path of a file where to write standard output. | | stderr | Path of a file where to write standard error. | | alias | Alias given to the process. | Note that because ``**configuration`` is passed using ``name=value`` syntax, possible equal signs in other arguments passed to `Run Process` and `Start Process` must be escaped with a backslash like ``name\\=value``. See `Run Process` for an example. == Running processes in shell == The ``shell`` argument specifies whether to run the process in a shell or not. By default shell is not used, which means that shell specific commands, like ``copy`` and ``dir`` on Windows, are not available. You can, however, run shell scripts and batch files without using a shell. Giving the ``shell`` argument any non-false value, such as ``shell=True``, changes the program to be executed in a shell. It allows using the shell capabilities, but can also make the process invocation operating system dependent. Having a shell between the actually started process and this library can also interfere communication with the process such as stopping it and reading its outputs. Because of these problems, it is recommended to use the shell only when absolutely necessary. When using a shell it is possible to give the whole command to execute as a single string. See `Specifying command and arguments` section for examples and more details in general. == Current working directory == By default the child process will be executed in the same directory as the parent process, the process running tests, is executed. This can be changed by giving an alternative location using the ``cwd`` argument. Forward slashes in the given path are automatically converted to backslashes on Windows. `Standard output and error streams`, when redirected to files, are also relative to the current working directory possibly set using the ``cwd`` argument. Example: | `Run Process` | prog.exe | cwd=${ROOT}/directory | stdout=stdout.txt | == Environment variables == By default the child process will get a copy of the parent process's environment variables. The ``env`` argument can be used to give the child a custom environment as a Python dictionary. If there is a need to specify only certain environment variable, it is possible to use the ``env:<name>=<value>`` format to set or override only that named variables. It is also possible to use these two approaches together. Examples: | `Run Process` | program | env=${environ} | | `Run Process` | program | env:http_proxy=10.144.1.10:8080 | env:PATH=%{PATH}${:}${PROGDIR} | | `Run Process` | program | env=${environ} | env:EXTRA=value | == Standard output and error streams == By default processes are run so that their standard output and standard error streams are kept in the memory. This works fine normally, but if there is a lot of output, the output buffers may get full and the program can hang. Additionally on Jython, everything written to these in-memory buffers can be lost if the process is terminated. To avoid the above mentioned problems, it is possible to use ``stdout`` and ``stderr`` arguments to specify files on the file system where to redirect the outputs. This can also be useful if other processes or other keywords need to read or manipulate the outputs somehow. Given ``stdout`` and ``stderr`` paths are relative to the `current working directory`. Forward slashes in the given paths are automatically converted to backslashes on Windows. As a special feature, it is possible to redirect the standard error to the standard output by using ``stderr=STDOUT``. Regardless are outputs redirected to files or not, they are accessible through the `result object` returned when the process ends. Examples: | ${result} = | `Run Process` | program | stdout=${TEMPDIR}/stdout.txt | stderr=${TEMPDIR}/stderr.txt | | `Log Many` | stdout: ${result.stdout} | stderr: ${result.stderr} | | ${result} = | `Run Process` | program | stderr=STDOUT | | `Log` | all output: ${result.stdout} | Note that the created output files are not automatically removed after the test run. The user is responsible to remove them if needed. == Alias == A custom name given to the process that can be used when selecting the `active process`. Examples: | `Start Process` | program | alias=example | | `Run Process` | python | -c | print 'hello' | alias=hello | = Active process = The test library keeps record which of the started processes is currently active. By default it is latest process started with `Start Process`, but `Switch Process` can be used to select a different one. Using `Run Process` does not affect the active process. The keywords that operate on started processes will use the active process by default, but it is possible to explicitly select a different process using the ``handle`` argument. The handle can be the identifier returned by `Start Process` or an ``alias`` explicitly given to `Start Process` or `Run Process`. = Result object = `Run Process`, `Wait For Process` and `Terminate Process` keywords return a result object that contains information about the process execution as its attributes. The same result object, or some of its attributes, can also be get using `Get Process Result` keyword. Attributes available in the object are documented in the table below. | = Attribute = | = Explanation = | | rc | Return code of the process as an integer. | | stdout | Contents of the standard output stream. | | stderr | Contents of the standard error stream. | | stdout_path | Path where stdout was redirected or ``None`` if not redirected. | | stderr_path | Path where stderr was redirected or ``None`` if not redirected. | Example: | ${result} = | `Run Process` | program | | `Should Be Equal As Integers` | ${result.rc} | 0 | | `Should Match` | ${result.stdout} | Some t?xt* | | `Should Be Empty` | ${result.stderr} | | | ${stdout} = | `Get File` | ${result.stdout_path} | | `Should Be Equal` | ${stdout} | ${result.stdout} | | `File Should Be Empty` | ${result.stderr_path} | | = Boolean arguments = Some keywords accept arguments that are handled as Boolean values true or false. If such an argument is given as a string, it is considered false if it is either empty or case-insensitively equal to ``false`` or ``no``. Other strings are considered true regardless their value, and other argument types are tested using same [http://docs.python.org/2/library/stdtypes.html#truth-value-testing|rules as in Python]. True examples: | `Terminate Process` | kill=True | # Strings are generally true. | | `Terminate Process` | kill=yes | # Same as the above. | | `Terminate Process` | kill=${TRUE} | # Python ``True`` is true. | | `Terminate Process` | kill=${42} | # Numbers other than 0 are true. | False examples: | `Terminate Process` | kill=False | # String ``false`` is false. | | `Terminate Process` | kill=no | # Also string ``no`` is false. | | `Terminate Process` | kill=${EMPTY} | # Empty string is false. | | `Terminate Process` | kill=${FALSE} | # Python ``False`` is false. | Note that prior to Robot Framework 2.8 all non-empty strings, including ``false``, were considered true. Additionally, ``no`` is considered false only in Robot Framework 2.9 and newer. = Example = | ***** Settings ***** | Library Process | Suite Teardown `Terminate All Processes` kill=True | | ***** Test Cases ***** | Example | `Start Process` program arg1 arg2 alias=First | ${handle} = `Start Process` command.sh arg | command2.sh shell=True cwd=/path | ${result} = `Run Process` ${CURDIR}/script.py | `Should Not Contain` ${result.stdout} FAIL | `Terminate Process` ${handle} | ${result} = `Wait For Process` First | `Should Be Equal As Integers` ${result.rc} 0 """ ROBOT_LIBRARY_SCOPE = "GLOBAL" ROBOT_LIBRARY_VERSION = get_version() TERMINATE_TIMEOUT = 30 KILL_TIMEOUT = 10 def __init__(self): self._processes = ConnectionCache("No active process.") self._results = {} def run_process(self, command, *arguments, **configuration): """Runs a process and waits for it to complete. ``command`` and ``*arguments`` specify the command to execute and arguments passed to it. See `Specifying command and arguments` for more details. ``**configuration`` contains additional configuration related to starting processes and waiting for them to finish. See `Process configuration` for more details about configuration related to starting processes. Configuration related to waiting for processes consists of ``timeout`` and ``on_timeout`` arguments that have same semantics as with `Wait For Process` keyword. By default there is no timeout, and if timeout is defined the default action on timeout is ``terminate``. Returns a `result object` containing information about the execution. Note that possible equal signs in ``*arguments`` must be escaped with a backslash (e.g. ``name\\=value``) to avoid them to be passed in as ``**configuration``. Examples: | ${result} = | Run Process | python | -c | print 'Hello, world!' | | Should Be Equal | ${result.stdout} | Hello, world! | | ${result} = | Run Process | ${command} | stderr=STDOUT | timeout=10s | | ${result} = | Run Process | ${command} | timeout=1min | on_timeout=continue | | ${result} = | Run Process | java -Dname\\=value Example | shell=True | cwd=${EXAMPLE} | This keyword does not change the `active process`. ``timeout`` and ``on_timeout`` arguments are new in Robot Framework 2.8.4. """ current = self._processes.current timeout = configuration.pop("timeout", None) on_timeout = configuration.pop("on_timeout", "terminate") try: handle = self.start_process(command, *arguments, **configuration) return self.wait_for_process(handle, timeout, on_timeout) finally: self._processes.current = current def start_process(self, command, *arguments, **configuration): """Starts a new process on background. See `Specifying command and arguments` and `Process configuration` for more information about the arguments, and `Run Process` keyword for related examples. Makes the started process new `active process`. Returns an identifier that can be used as a handle to active the started process if needed. Starting from Robot Framework 2.8.5, processes are started so that they create a new process group. This allows sending signals to and terminating also possible child processes. This is not supported by Jython in general nor by Python versions prior to 2.7 on Windows. """ config = ProcessConfig(**configuration) command = self._get_command(command, arguments, config.shell) self._log_start(command, config) process = subprocess.Popen(command, **config.full_config) self._results[process] = ExecutionResult(process, config.stdout_stream, config.stderr_stream) return self._processes.register(process, alias=config.alias) def _get_command(self, command, args, use_shell): command = [encode_to_system(item) for item in [command] + list(args)] if not use_shell: return command if args: return subprocess.list2cmdline(command) return command[0] def _log_start(self, command, config): if is_list_like(command): command = self.join_command_line(command) logger.info("Starting process:\n%s" % command) logger.debug("Process configuration:\n%s" % config) def is_process_running(self, handle=None): """Checks is the process running or not. If ``handle`` is not given, uses the current `active process`. Returns ``True`` if the process is still running and ``False`` otherwise. """ return self._processes[handle].poll() is None def process_should_be_running(self, handle=None, error_message="Process is not running."): """Verifies that the process is running. If ``handle`` is not given, uses the current `active process`. Fails if the process has stopped. """ if not self.is_process_running(handle): raise AssertionError(error_message) def process_should_be_stopped(self, handle=None, error_message="Process is running."): """Verifies that the process is not running. If ``handle`` is not given, uses the current `active process`. Fails if the process is still running. """ if self.is_process_running(handle): raise AssertionError(error_message) def wait_for_process(self, handle=None, timeout=None, on_timeout="continue"): """Waits for the process to complete or to reach the given timeout. The process to wait for must have been started earlier with `Start Process`. If ``handle`` is not given, uses the current `active process`. ``timeout`` defines the maximum time to wait for the process. It can be given in [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#time-format| various time formats] supported by Robot Framework, for example, ``42``, ``42 s``, or ``1 minute 30 seconds``. ``on_timeout`` defines what to do if the timeout occurs. Possible values and corresponding actions are explained in the table below. Notice that reaching the timeout never fails the test. | = Value = | = Action = | | continue | The process is left running (default). | | terminate | The process is gracefully terminated. | | kill | The process is forcefully stopped. | See `Terminate Process` keyword for more details how processes are terminated and killed. If the process ends before the timeout or it is terminated or killed, this keyword returns a `result object` containing information about the execution. If the process is left running, Python ``None`` is returned instead. Examples: | # Process ends cleanly | | | | ${result} = | Wait For Process | example | | Process Should Be Stopped | example | | | Should Be Equal As Integers | ${result.rc} | 0 | | # Process does not end | | | | ${result} = | Wait For Process | timeout=42 secs | | Process Should Be Running | | | | Should Be Equal | ${result} | ${NONE} | | # Kill non-ending process | | | | ${result} = | Wait For Process | timeout=1min 30s | on_timeout=kill | | Process Should Be Stopped | | | | Should Be Equal As Integers | ${result.rc} | -9 | ``timeout`` and ``on_timeout`` are new in Robot Framework 2.8.2. """ process = self._processes[handle] logger.info("Waiting for process to complete.") if timeout: timeout = timestr_to_secs(timeout) if not self._process_is_stopped(process, timeout): logger.info("Process did not complete in %s." % secs_to_timestr(timeout)) return self._manage_process_timeout(handle, on_timeout.lower()) return self._wait(process) def _manage_process_timeout(self, handle, on_timeout): if on_timeout == "terminate": return self.terminate_process(handle) elif on_timeout == "kill": return self.terminate_process(handle, kill=True) else: logger.info("Leaving process intact.") return None def _wait(self, process): result = self._results[process] result.rc = process.wait() or 0 result.close_streams() logger.info("Process completed.") return result def terminate_process(self, handle=None, kill=False): """Stops the process gracefully or forcefully. If ``handle`` is not given, uses the current `active process`. By default first tries to stop the process gracefully. If the process does not stop in 30 seconds, or ``kill`` argument is given a true value, (see `Boolean arguments`) kills the process forcefully. Stops also all the child processes of the originally started process. Waits for the process to stop after terminating it. Returns a `result object` containing information about the execution similarly as `Wait For Process`. On Unix-like machines graceful termination is done using ``TERM (15)`` signal and killing using ``KILL (9)``. Use `Send Signal To Process` instead if you just want to send either of these signals without waiting for the process to stop. On Windows graceful termination is done using ``CTRL_BREAK_EVENT`` event and killing using Win32 API function ``TerminateProcess()``. Examples: | ${result} = | Terminate Process | | | Should Be Equal As Integers | ${result.rc} | -15 | # On Unixes | | Terminate Process | myproc | kill=true | Limitations: - Graceful termination is not supported on Windows by Jython nor by Python versions prior to 2.7. Process is killed instead. - Stopping the whole process group is not supported by Jython at all nor by Python versions prior to 2.7 on Windows. - On Windows forceful kill only stops the main process, not possible child processes. Automatically killing the process if termination fails as well as returning a result object are new features in Robot Framework 2.8.2. Terminating also possible child processes, including using ``CTRL_BREAK_EVENT`` on Windows, is new in Robot Framework 2.8.5. """ process = self._processes[handle] if not hasattr(process, "terminate"): raise RuntimeError("Terminating processes is not supported " "by this Python version.") terminator = self._kill if is_truthy(kill) else self._terminate try: terminator(process) except OSError: if not self._process_is_stopped(process, self.KILL_TIMEOUT): raise logger.debug("Ignored OSError because process was stopped.") return self._wait(process) def _kill(self, process): logger.info("Forcefully killing process.") if hasattr(os, "killpg"): os.killpg(process.pid, signal_module.SIGKILL) else: process.kill() if not self._process_is_stopped(process, self.KILL_TIMEOUT): raise RuntimeError("Failed to kill process.") def _terminate(self, process): logger.info("Gracefully terminating process.") # Sends signal to the whole process group both on POSIX and on Windows # if supported by the interpreter. if hasattr(os, "killpg"): os.killpg(process.pid, signal_module.SIGTERM) elif hasattr(signal_module, "CTRL_BREAK_EVENT"): if IRONPYTHON: # https://ironpython.codeplex.com/workitem/35020 ctypes.windll.kernel32.GenerateConsoleCtrlEvent(signal_module.CTRL_BREAK_EVENT, process.pid) else: process.send_signal(signal_module.CTRL_BREAK_EVENT) else: process.terminate() if not self._process_is_stopped(process, self.TERMINATE_TIMEOUT): logger.info("Graceful termination failed.") self._kill(process) def terminate_all_processes(self, kill=False): """Terminates all still running processes started by this library. This keyword can be used in suite teardown or elsewhere to make sure that all processes are stopped, By default tries to terminate processes gracefully, but can be configured to forcefully kill them immediately. See `Terminate Process` that this keyword uses internally for more details. """ for handle in range(1, len(self._processes) + 1): if self.is_process_running(handle): self.terminate_process(handle, kill=kill) self.__init__() def send_signal_to_process(self, signal, handle=None, group=False): """Sends the given ``signal`` to the specified process. If ``handle`` is not given, uses the current `active process`. Signal can be specified either as an integer as a signal name. In the latter case it is possible to give the name both with or without ``SIG`` prefix, but names are case-sensitive. For example, all the examples below send signal ``INT (2)``: | Send Signal To Process | 2 | | # Send to active process | | Send Signal To Process | INT | | | | Send Signal To Process | SIGINT | myproc | # Send to named process | This keyword is only supported on Unix-like machines, not on Windows. What signals are supported depends on the system. For a list of existing signals on your system, see the Unix man pages related to signal handling (typically ``man signal`` or ``man 7 signal``). By default sends the signal only to the parent process, not to possible child processes started by it. Notice that when `running processes in shell`, the shell is the parent process and it depends on the system does the shell propagate the signal to the actual started process. To send the signal to the whole process group, ``group`` argument can be set to any true value (see `Boolean arguments`). This is not supported by Jython, however. New in Robot Framework 2.8.2. Support for ``group`` argument is new in Robot Framework 2.8.5. """ if os.sep == "\\": raise RuntimeError("This keyword does not work on Windows.") process = self._processes[handle] signum = self._get_signal_number(signal) logger.info("Sending signal %s (%d)." % (signal, signum)) if is_truthy(group) and hasattr(os, "killpg"): os.killpg(process.pid, signum) elif hasattr(process, "send_signal"): process.send_signal(signum) else: raise RuntimeError("Sending signals is not supported " "by this Python version.") def _get_signal_number(self, int_or_name): try: return int(int_or_name) except ValueError: return self._convert_signal_name_to_number(int_or_name) def _convert_signal_name_to_number(self, name): try: return getattr(signal_module, name if name.startswith("SIG") else "SIG" + name) except AttributeError: raise RuntimeError("Unsupported signal '%s'." % name) def get_process_id(self, handle=None): """Returns the process ID (pid) of the process as an integer. If ``handle`` is not given, uses the current `active process`. Notice that the pid is not the same as the handle returned by `Start Process` that is used internally by this library. """ return self._processes[handle].pid def get_process_object(self, handle=None): """Return the underlying ``subprocess.Popen`` object. If ``handle`` is not given, uses the current `active process`. """ return self._processes[handle] def get_process_result( self, handle=None, rc=False, stdout=False, stderr=False, stdout_path=False, stderr_path=False ): """Returns the specified `result object` or some of its attributes. The given ``handle`` specifies the process whose results should be returned. If no ``handle`` is given, results of the current `active process` are returned. In either case, the process must have been finishes before this keyword can be used. In practice this means that processes started with `Start Process` must be finished either with `Wait For Process` or `Terminate Process` before using this keyword. If no other arguments than the optional ``handle`` are given, a whole `result object` is returned. If one or more of the other arguments are given any true value, only the specified attributes of the `result object` are returned. These attributes are always returned in the same order as arguments are specified in the keyword signature. See `Boolean arguments` section for more details about true and false values. Examples: | Run Process | python | -c | print 'Hello, world!' | alias=myproc | | # Get result object | | | | ${result} = | Get Process Result | myproc | | Should Be Equal | ${result.rc} | ${0} | | Should Be Equal | ${result.stdout} | Hello, world! | | Should Be Empty | ${result.stderr} | | | # Get one attribute | | | | ${stdout} = | Get Process Result | myproc | stdout=true | | Should Be Equal | ${stdout} | Hello, world! | | # Multiple attributes | | | | ${stdout} | ${stderr} = | Get Process Result | myproc | stdout=yes | stderr=yes | | Should Be Equal | ${stdout} | Hello, world! | | Should Be Empty | ${stderr} | | Although getting results of a previously executed process can be handy in general, the main use case for this keyword is returning results over the remote library interface. The remote interface does not support returning the whole result object, but individual attributes can be returned without problems. New in Robot Framework 2.8.2. """ result = self._results[self._processes[handle]] if result.rc is None: raise RuntimeError("Getting results of unfinished processes " "is not supported.") attributes = self._get_result_attributes(result, rc, stdout, stderr, stdout_path, stderr_path) if not attributes: return result elif len(attributes) == 1: return attributes[0] return attributes def _get_result_attributes(self, result, *includes): attributes = (result.rc, result.stdout, result.stderr, result.stdout_path, result.stderr_path) includes = (is_truthy(incl) for incl in includes) return tuple(attr for attr, incl in zip(attributes, includes) if incl) def switch_process(self, handle): """Makes the specified process the current `active process`. The handle can be an identifier returned by `Start Process` or the ``alias`` given to it explicitly. Example: | Start Process | prog1 | alias=process1 | | Start Process | prog2 | alias=process2 | | # currently active process is process2 | | Switch Process | process1 | | # now active process is process1 | """ self._processes.switch(handle) def _process_is_stopped(self, process, timeout): stopped = lambda: process.poll() is not None max_time = time.time() + timeout while time.time() <= max_time and not stopped(): time.sleep(min(0.1, timeout)) return stopped() def split_command_line(self, args, escaping=False): """Splits command line string into a list of arguments. String is split from spaces, but argument surrounded in quotes may contain spaces in them. If ``escaping`` is given a true value, then backslash is treated as an escape character. It can escape unquoted spaces, quotes inside quotes, and so on, but it also requires using double backslashes when using Windows paths. Examples: | @{cmd} = | Split Command Line | --option "value with spaces" | | Should Be True | $cmd == ['--option', 'value with spaces'] | New in Robot Framework 2.9.2. """ return cmdline2list(args, escaping=escaping) def join_command_line(self, *args): """Joins arguments into one command line string. In resulting command line string arguments are delimited with a space, arguments containing spaces are surrounded with quotes, and possible quotes are escaped with a backslash. If this keyword is given only one argument and that is a list like object, then the values of that list are joined instead. Example: | ${cmd} = | Join Command Line | --option | value with spaces | | Should Be Equal | ${cmd} | --option "value with spaces" | New in Robot Framework 2.9.2. """ if len(args) == 1 and is_list_like(args[0]): args = args[0] return subprocess.list2cmdline(args)
class ProcessLibrary(object): def __init__(self): self._started_processes = ConnectionCache() self._logs = dict() self._tempdir = tempfile.mkdtemp(suffix="processlib") def run_process(self, command, *args, **conf): p = self.start_new_process(command, *args, **conf) return self.wait_for_process(p) def start_new_process(self, command, *args, **conf): cmd = [command] + [str(i) for i in args] stdout_stream = open(conf['stdout'], 'w') if 'stdout' in conf else self._get_temp_file() stderr_stream = open(conf['stderr'], 'w') if 'stderr' in conf else self._get_temp_file("stderr") print "stdout tempfile is", stdout_stream.name print "stderr tempfile is", stderr_stream.name print "args", args pd = ProcessData(stdout_stream.name, stderr_stream.name) if 'shell' in conf: use_shell = (conf['shell'] != 'False') else: use_shell = False if 'cwd' in conf: cwd = conf['cwd'] else: cwd = '.' if use_shell and args: cmd = "exec " + subprocess.list2cmdline(cmd) print cmd p = subprocess.Popen(cmd, stdout=stdout_stream, stderr=stderr_stream, shell=use_shell, cwd=cwd) print p.pid if 'alias' in conf: alias = conf['alias'] else: alias = None index = self._started_processes.register(p, alias=alias) self._logs[index] = pd return index def process_is_alive(self, handle=None): if handle: self._started_processes.switch(handle) return self._started_processes.current.poll() is None def process_should_be_alive(self, handle): if not self.process_is_alive(handle): raise AssertionError('Process is not alive') def process_should_be_dead(self, handle): if self.process_is_alive(handle): raise AssertionError('Process is alive') def wait_for_process(self, handle=None): if handle: self._started_processes.switch(handle) exit_code = self._started_processes.current.wait() logs = self._logs[handle] with open(logs.stdout, 'r') as f: stdout = f.read() with open(logs.stderr, 'r') as f: stderr = f.read() return ExecutionResult(stdout, stderr, exit_code) def kill_process(self, handle=None): if handle: self._started_processes.switch(handle) self._started_processes.current.kill() def terminate_process(self, handle=None): if handle: self._started_processes.switch(handle) self._started_processes.current.terminate() def kill_all_processes(self): for handle in range(len(self._started_processes._connections)): self.kill_process(handle) def get_pid(self, handle): self._started_processes.switch(handle) return self._started_processes.current.pid def _get_temp_file(self, suffix="stdout"): return tempfile.NamedTemporaryFile(delete=False, prefix='tmp_logfile_', suffix="_%s" % suffix, dir=self._tempdir) def input_to_process(self, handle, msg): if not msg: return alog = self._logs[handle] self._started_processes.switch(handle) self._started_processes.current.wait() with open(alog.stdout, 'a') as f: f.write(msg.encode('UTF-8'))
class SSHLibrary(DeprecatedSSHLibraryKeywords): """Robot Framework test library for SSH and SFTP. SSHLibrary works with both Python and Jython interpreters. To use SSHLibrary with Python, you must first install paramiko SSH implementation[1] and its dependencies. For Jython, you must have jar distribution of Trilead SSH implementation[2] in the CLASSPATH during test execution | [1] http://www.lag.net/paramiko/ | [2] http://www.trilead.com/Products/Trilead_SSH_for_Java/ The library supports multiple connections to different hosts. A connection must always be opened using `Open Connection` before the other keywords work. For executing commands, there are two possibilities: 1. `Execute Command` or `Start Command`. These keywords open a new session using the connection, possible state changes are not preserved. 2. Keywords `Write` and `Read XXX` operate in an interactive shell, which means that changes to state are visible to next keywords. Note that in interactive mode, a prompt must be set before using any of the Write-keywords. Prompt can be set either on `library importing` or when a new connection is opened using `Open Connection`. """ ROBOT_LIBRARY_SCOPE = 'GLOBAL' ROBOT_LIBRARY_VERSION = __version__ def __init__(self, timeout=3, newline='LF', prompt=None, loglevel='INFO'): """SSH Library allows some import time configuration. `timeout`, `newline` and `prompt` set default values for new connections opened with `Open Connection`. The default values may later be changed using `Set Default Configuration` and settings of a single connection with `Set Client Configuration`. `loglevel` sets the default log level used to log return values of `Read Until` variants. It can also be later changed using `Set Default Configuration`. Examples: | Library | SSHLibrary | # use default values | | Library | SSHLibrary | timeout=10 | prompt=> | """ self._cache = ConnectionCache() self._config = DefaultConfig(timeout, newline, prompt, loglevel) @property def ssh_client(self): return self._cache.current def set_default_configuration(self, timeout=None, newline=None, prompt=None, loglevel=None): """Update the default configuration values. Only parameters whose value is other than `None` are updated. Example: | Set Default Configuration | newline=CRLF | prompt=$ | """ self._config.update(timeout=timeout, newline=newline, prompt=prompt, loglevel=loglevel) def set_client_configuration(self, timeout=None, newline=None, prompt=None, term_type='vt100', width=80, height=24): """Update the client configuration values. Works on the currently selected connection. At least one connection must have been opened using `Open Connection`. Only parameters whose value is other than `None` are updated. Example: | Set Client Configuration | term_type=ansi | timeout=2 hours | """ self.ssh_client.config.update(timeout=timeout, newline=newline, prompt=prompt, term_type=term_type, width=width, height=height) def open_connection(self, host, alias=None, port=22, timeout=None, newline=None, prompt=None, term_type='vt100', width=80, height=24): """Opens a new SSH connection to given `host` and `port`. Possible already opened connections are cached. Returns the index of this connection which can be used later to switch back to it. Indexing starts from 1 and is reset when `Close All` keyword is used. Optional `alias` is a name for the connection and it can be used for switching between connections similarly as the index. See `Switch Connection` for more details about that. If `timeout`, `newline` or `prompt` are not given, the default values set in `library importing` are used. See also `Set Default Configuration`. Starting from SSHLibrary 1.1, a shell session is also opened automatically by this keyword. `term_type` defines the terminal type for this shell, and `width` and `height` can be configured to control the virtual size of it. Client configuration options other than `host`, `port` and `alias` can be later updated using `Set Client Configuration`. Examples: | Open Connection | myhost.net | | Open Connection | yourhost.com | alias=2nd conn | port=23 |prompt=# | | Open Connection | myhost.net | term_type=ansi | width=40 | | ${id} = | Open Connection | myhost.net | """ timeout = timeout or self._config.timeout newline = newline or self._config.newline prompt = prompt or self._config.prompt client = SSHClient(host, alias, port, timeout, newline, prompt, term_type, width, height) return self._cache.register(client, alias) def switch_connection(self, index_or_alias): """Switches between active connections using index or alias. Index is got from `Open Connection` and alias can be given to it. Returns the index of previous connection, which can be used to restore the connection later. Example: | Open Connection | myhost.net | | | Login | john | secret | | Execute Command | some command | | | Open Connection | yourhost.com | 2nd conn | | Login | root | password | | Start Command | another cmd | | | Switch Connection | 1 | # index | | Execute Command | something | | | Switch Connection | 2nd conn | # alias | | Read Command Output | | | | Close All Connections | | | Above example expects that there was no other open connections when opening the first one because it used index '1' when switching to it later. If you aren't sure about that you can store the index into a variable as below. | ${id} = | Open Connection | myhost.net | | # Do something ... | | Switch Connection | ${id} | | """ old_index = self._cache.current_index self._cache.switch(index_or_alias) return old_index def close_all_connections(self): """Closes all open connections and empties the connection cache. After this keyword indices returned by `Open Connection` start from 1. This keyword ought to be used in test or suite teardown to make sure all connections are closed. """ self._cache.close_all() def get_connections(self): """Return information about opened connections. The return value is a list of objects that describe the connection. These objects have attributes that correspond to the argument names of `Open Connection`. Connection information is also logged. Example: | Open Connection | somehost | prompt=>> | | Open Connection | otherhost | timeout=5 minutes | | ${conn1} | ${conn2}= | Get Connections | | Should Be Equal | ${conn1.host} | somehost | | Should Be Equal | ${conn2.timeout} | 5 minutes | """ # TODO: could the ConnectionCache be enhanced to be iterable? configs = [c.config for c in self._cache._connections] for c in configs: self._log(str(c)) return configs def enable_ssh_logging(self, logfile): """Enables logging of SSH protocol output to given `logfile` `logfile` can be relative or absolute path to a file that is writable by current user. In case that it already exists, it will be overwritten. Note that this keyword only works with Python, e.g. when executing the tests with `pybot`. """ if SSHClient.enable_logging(logfile): self._log('SSH log is written to <a href="%s">file</a>.' % logfile, 'HTML') def close_connection(self): """Closes the currently active connection.""" self.ssh_client.close() def login(self, username, password): """Logs in to SSH server with given user information. Reads and returns available output. If prompt is set, everything until the prompt is returned. Example: | Login | john | secret | """ return self._login(self.ssh_client.login, username, password) def login_with_public_key(self, username, keyfile, password): """Logs into SSH server with using key-based authentication. `username` is the username on the remote system. `keyfile` is a path to a valid OpenSSH private key file. `password` is used to unlock `keyfile` if unlocking is required. Reads and returns available output. If prompt is set, everything until the prompt is returned. """ return self._login(self.ssh_client.login_with_public_key, username, keyfile, password) def _login(self, login_method, username, *args): self._info("Logging into '%s:%s' as '%s'." % (self.ssh_client.host, self.ssh_client.port, username)) try: return login_method(username, *args) except SSHClientException, e: raise RuntimeError(e)
class PysphereLibrary(object): """Robot Framework test library for VMWare interaction The library has the following main usages: - Identifying available virtual machines on a vCenter or ESXi host - Starting and stopping VMs - Shutting down, rebooting VM guest OS - Checking VM status - Waiting for VM tools to start running - Reverting VMs to a snapshot - Retrieving basic VM properties - File upload, deletion and relocation - Directory creation, deletion and relocation - Process execution and termination This library is essentially a wrapper around Pysphere http://code.google.com/p/pysphere/ adding connection caching consistent with other Robot Framework libraries. """ ROBOT_LIBRARY_SCOPE = 'GLOBAL' ROBOT_LIBRARY_VERSION = VERSION def __init__(self): """ """ self._connections = ConnectionCache() self._vm_cache = {} def open_pysphere_connection(self, host, user, password, alias=None): """Opens a pysphere connection to the given `host` using the supplied `user` and `password`. The new connection is made active and any existing connections are left open in the background. This keyword returns the index of the new connection which can be used later to switch back to it. Indices start from `1` and are reset when `Close All Pysphere Connections` is called. An optional `alias` can be supplied for the connection and used for switching between connections. See `Switch Pysphere Connection` for details. Example: | ${index}= | Open Pysphere Connection | my.vcenter.server.com | username | password | alias=myserver | """ server = VIServer() server.connect(host, user, password) connection_index = self._connections.register(server, alias) logger.info("Pysphere connection opened to host {}".format(host)) return connection_index def is_connected_to_pysphere(self): return self._connections.current.is_connected() def switch_pysphere_connection(self, index_or_alias): """Switches the active connection by index of alias. `index_or_alias` is either a connection index (an integer) or alias (a string). Index can be obtained by capturing the return value from `Open Pysphere Connection` and alias can be set as a named variable with the same method. Example: | ${my_connection}= | Open Pysphere Connection | myhost | myuser | mypassword | | Open Pysphere Connection | myotherhost | myuser | mypassword | alias=otherhost | | Switch Pysphere Connection | ${my_connection} | | Power On Vm | myvm | | Switch Pysphere Connection | otherhost | | Power On Vm | myothervm | """ old_index = self._connections.current_index if index_or_alias is not None: self._connections.switch(index_or_alias) logger.info(u"Pysphere connection switched to {}".format(index_or_alias)) else: logger.info("No index or alias given, pysphere connection has not been switched.") def close_pysphere_connection(self): """Closes the current pysphere connection. No other connection is made active by this keyword. use `Switch Pysphere Connection` to switch to another connection. Example: | ${my_connection}= | Open Pysphere Connection | myhost | myuser | mypassword | | Power On Vm | myvm | | Close Pysphere Connection | """ self._connections.current.disconnect() logger.info("Connection closed, there will no longer be a current pysphere connection.") self._connections.current = self._connections._no_current def close_all_pysphere_connections(self): """Closes all active pysphere connections. This keyword is appropriate for use in test or suite teardown. The assignment of connection indices resets after calling this keyword, and the next connection opened will be allocated index `1`. Example: | ${my_connection}= | Open Pysphere Connection | myhost | myuser | mypassword | | Open Pysphere Connection | myotherhost | myuser | mypassword | alias=otherhost | | Switch Pysphere Connection | ${myserver} | | Power On Vm | myvm | | Switch Pysphere Connection | otherhost | | Power On Vm | myothervm | | [Teardown] | Close All Pysphere Connections | """ self._connections.close_all(closer_method='disconnect') logger.info("All pysphere connections closed.") def get_vm_names(self): """Returns a list of all registered VMs for the currently active connection. Example: | Open Pysphere Connection | myhost | myuser | mypassword | | @{vm_names}= | Get Vm Names | """ return self._connections.current.get_registered_vms() def get_vm_properties(self, name): """Returns a dictionary of the properties associated with the named VM. """ vm = self._get_vm(name) return vm.get_properties(from_cache=False) def power_on_vm(self, name): """Power on the vm if it is not already running. This method blocks until the operation is completed. """ if not self.vm_is_powered_on(name): vm = self._get_vm(name) vm.power_on() logger.info(u"VM {} powered on.".format(name)) else: logger.info(u"VM {} was already powered on.".format(name)) def power_off_vm(self, name): """Power off the vm if it is not already powered off. This method blocks until the operation is completed. """ if not self.vm_is_powered_off(name): vm = self._get_vm(name) vm.power_off() logger.info(u"VM {} powered off.".format(name)) else: logger.info(u"VM {} was already powered off.".format(name)) def reset_vm(self, name): """Perform a reset on the VM. This method blocks until the operation is completed. """ vm = self._get_vm(name) vm.reset() logger.info(u"VM {} reset.".format(name)) def shutdown_vm_os(self, name): """Initiate a shutdown in the guest OS in the VM, returning immediately. """ vm = self._get_vm(name) vm.shutdown_guest() logger.info(u"VM {} shutdown initiated.".format(name)) def reboot_vm_os(self, name): """Initiate a reboot in the guest OS in the VM, returning immediately. """ vm = self._get_vm(name) vm.reboot_guest() logger.info(u"VM {} reboot initiated.".format(name)) def vm_is_powered_on(self, name): """Returns true if the VM is in the powered on state. """ vm = self._get_vm(name) return vm.is_powered_on() def vm_is_powered_off(self, name): """Returns true if the VM is in the powered off state. """ vm = self._get_vm(name) return vm.is_powered_off() def vm_wait_for_tools(self, name, timeout=120): """Waits for up to the `timeout` interval for the VM tools to start running on the named VM. VMware tools must be running on the VM for the `Vm Login In Guest` keyword to succeed. """ vm = self._get_vm(name) vm.wait_for_tools(timeout) logger.info(u"VM tools are running on {}.".format(name)) def vm_login_in_guest(self, name, username, password): """Logs into the named VM with the specified `username` and `password`. The VM must be powered on and the VM tools must be running on the VM, which can be verified using the `Vm Wait For Tools` keyword. Example: | Open Pysphere Connection | myhost | myuser | mypassword | | Power On Vm | myvm | | Vm Wait For Tools | myvm | | Vm Login In Guest | myvm | vm_username | vm_password | """ vm = self._get_vm(name) vm.login_in_guest(username, password) logger.info(u"Logged into VM {}.".format(name)) def vm_make_directory(self, name, path): """Creates a directory with the specified `path` on the named VM. The `Vm Login In Guest` keyword must precede this keyword. Example: | Open Pysphere Connection | myhost | myuser | mypassword | | Vm Login In Guest | myvm | vm_username | vm_password | | Vm Make Directory | myvm | C:\\some\\directory\\path | """ vm = self._get_vm(name) vm.make_directory(path, True) logger.info(u"Created directory {} on VM {}.".format(path, name)) def vm_move_directory(self, name, src_path, dst_path): """Moves or renames a directory from `src_path` to `dst_path` on the named VM. The `Vm Login In Guest` keyword must precede this keyword. Example: | Open Pysphere Connection | myhost | myuser | mypassword | | Vm Login In Guest | myvm | vm_username | vm_password | | Vm Move Directory | myvm | C:\\directory1 | C:\\directory2 | """ vm = self._get_vm(name) vm.move_directory(src_path, dst_path) logger.info(u"Moved directory {} to {} on VM {}.".format( src_path, dst_path, name)) def vm_delete_directory(self, name, path): """Deletes the directory with the given `path` on the named VM, including its contents. The `Vm Login In Guest` keyword must precede this keyword. Example: | Open Pysphere Connection | myhost | myuser | mypassword | | Vm Login In Guest | myvm | vm_username | vm_password | | Vm Delete Directory | myvm | C:\\directory | """ vm = self._get_vm(name) vm.delete_directory(path, True) logger.info(u"Deleted directory {} on VM {}.".format(path, name)) def vm_get_file(self, name, remote_path, local_path): """Downloads a file from the `remote_path` on the named VM to the specified `local_path`, overwriting any existing local file. The `Vm Login In Guest` keyword must precede this keyword. Example: | Open Pysphere Connection | myhost | myuser | mypassword | | Vm Login In Guest | myvm | vm_username | vm_password | | Vm Get File | myvm | C:\\remote\\location.txt | C:\\local\\location.txt | """ vm = self._get_vm(name) vm.get_file(remote_path, local_path, True) logger.info(u"Downloaded file {} on VM {} to {}.".format( remote_path, name, local_path)) def vm_send_file(self, name, local_path, remote_path): """Uploads a file from `local_path` to the specified `remote_path` on the named VM, overwriting any existing remote file. The `Vm Login In Guest` keyword must precede this keyword. Example: | Open Pysphere Connection | myhost | myuser | mypassword | | Vm Login In Guest | myvm | vm_username | vm_password | | Vm Send File | myvm | C:\\local\\location.txt | C:\\remote\\location.txt | """ local_path = os.path.abspath(local_path) logger.info(u"Uploading file {} to {} on VM {}.".format( local_path, remote_path, name)) vm = self._get_vm(name) vm.send_file(local_path, remote_path, True) logger.info(u"Uploaded file {} to {} on VM {}.".format( local_path, remote_path, name)) def vm_move_file(self, name, src_path, dst_path): """Moves a remote file on the named VM from `src_path` to `dst_path`, overwriting any existing file at the target location. The `Vm Login In Guest` keyword must precede this keyword. Example: | Open Pysphere Connection | myhost | myuser | mypassword | | Vm Login In Guest | myvm | vm_username | vm_password | | Vm Move File | myvm | C:\\original_location.txt | C:\\new_location.txt | """ vm = self._get_vm(name) vm.move_file(src_path, dst_path, True) logger.info(u"Moved file from {} to {} on VM {}.".format( src_path, dst_path, name)) def vm_delete_file(self, name, remote_path): """Deletes the file with the given `remote_path` on the named VM. The `Vm Login In Guest` keyword must precede this keyword. Example: | Open Pysphere Connection | myhost | myuser | mypassword | | Vm Login In Guest | myvm | vm_username | vm_password | | Vm Delete File | myvm | C:\\remote_file.txt | """ vm = self._get_vm(name) vm.delete_file(remote_path) logger.info(u"Deleted file {} from VM {}.".format(remote_path, name)) def vm_start_process(self, name, cwd, program_path, *args, **kwargs): """Starts a program in the named VM with the working directory specified by `cwd`. Returns the process PID. The `Vm Login In Guest` keyword must precede this keyword. The optional `env` argument can be used to provide a dictionary containing environment variables to be set for the program being run. Example: | Open Pysphere Connection | myhost | myuser | mypassword | | Vm Login In Guest | myvm | vm_username | vm_password | | ${pid}= | Vm Start Process | myvm | C:\\ | C:\\windows\\system32\\cmd.exe | /c | echo | hello world | """ env = kwargs.get('env', None) logger.info(u"Starting process '{} {}' on VM {} cwd={} env={}".format( program_path, " ".join(args), name, cwd, env)) vm = self._get_vm(name) pid = vm.start_process(program_path, args, env, cwd) logger.info(u"Process '{} {}' running on VM {} with pid={} cwd={} env={}".format( program_path, " ".join(args), name, pid, cwd, env)) return pid def vm_run_synchronous_process(self, name, cwd, program_path, *args, **kwargs): """Executes a process on the named VM and blocks until the process has completed. Parameters are the same as for `vm_start_process`. Returns the exit code of the process. The `Vm Login In Guest` keyword must precede this keyword. Example: | Open Pysphere Connection | myhost | myuser | mypassword | | Vm Login In Guest | myvm | vm_username | vm_password | | ${rc}= | Vm Run Synchronous Process | myvm | C:\\ | C:\\windows\\system32\\cmd.exe | /c | echo | hello world | | Should Be Equal As Integers | ${rc} | 0 | """ pid = self.vm_start_process(name, cwd, program_path, *args, **kwargs) vm = self._get_vm(name) while True: processes = [x for x in vm.list_processes() if x["pid"] == pid] if len(processes) != 1: raise Exception("Process terminated and could not retrieve exit code") process = processes[0] if process['end_time'] != None: logger.info(u"Process completed on {}: {}".format(name, repr(process))) return process['exit_code'] time.sleep(2) def vm_terminate_process(self, name, pid): """Terminates the process with the given `pid` on the named VM. The `Vm Login In Guest` keyword must precede this keyword. Example: | Open Pysphere Connection | myhost | myuser | mypassword | | Vm Login In Guest | myvm | vm_username | vm_password | | ${pid}= | Vm Start Process | myvm | C:\\ | C:\\windows\\system32\\cmd.exe | /c | pause | | Vm Terminate Process | myvm | ${pid} | """ pid = int(pid) vm = self._get_vm(name) vm.terminate_process(pid) logger.info(u"Process with pid {} terminated on VM {}".format(pid, name)) def revert_vm_to_snapshot(self, name, snapshot_name=None): """Revert the named VM to a snapshot. If `snapshot_name` is supplied it is reverted to that snapshot, otherwise it is reverted to the current snapshot. This method blocks until the operation is completed. """ vm = self._get_vm(name) if snapshot_name is None: vm.revert_to_snapshot() logger.info(u"VM {} reverted to current snapshot.".format(name)) else: vm.revert_to_named_snapshot(snapshot_name) logger.info(u"VM {} reverted to snapshot {}.".format( name, snapshot_name)) def _get_vm(self, name): if name not in self._vm_cache or not self._vm_cache[name]._server.keep_session_alive(): logger.debug(u"VM {} not in cache or vcenter connection expired.".format(name)) connection = self._connections.current if isinstance(name, unicode): name = name.encode("utf8") self._vm_cache[name] = connection.get_vm_by_name(name) else: logger.debug(u"VM {} already in cache.".format(name)) return self._vm_cache[name]
class OracleDB(object): """ Robot Framework library for working with Oracle DB. == Dependencies == | cx_Oracle | http://cx-oracle.sourceforge.net | version >= 5.3 | | robot framework | http://robotframework.org | """ DEFAULT_TIMEOUT = 900.0 # The default timeout for executing an SQL query is 15 minutes ROBOT_LIBRARY_SCOPE = 'GLOBAL' last_executed_statement: Optional[str] = None last_executed_statement_params: Optional[Dict[str, Any]] = None last_used_connection_index: Optional[int] = None def __init__(self) -> None: """Library initialization. Robot Framework ConnectionCache() class is prepared for working with concurrent connections.""" self._connection: Optional[cx_Oracle.Connection] = None self._cache = ConnectionCache() @property def connection(self) -> cx_Oracle.Connection: """Get current connection to Oracle database. *Raises:*\n RuntimeError: if there isn't any open connection. *Returns:*\n Current connection to the database. """ if self._connection is None: raise RuntimeError( 'There is no open connection to Oracle database.') return self._connection def make_dsn(self, host: str, port: str, sid: str, service_name: str = '') -> str: """ Build dsn string for use in connection. *Args:*\n host - database host;\n port - database port;\n sid - database sid;\n service_name - database service name;\n *Returns:*\n Returns dsn string. """ return cx_Oracle.makedsn(host=host, port=port, sid=sid, service_name=service_name) def connect_to_oracle(self, dbname: str, dbusername: str, dbpassword: str = None, alias: str = None) -> int: """ Connection to Oracle DB. *Args:*\n _dbname_ - database name;\n _dbusername_ - username for db connection;\n _dbpassword_ - password for db connection;\n _alias_ - connection alias, used for switching between open connections;\n *Returns:*\n Returns ID of the new connection. The connection is set as active. *Example:*\n | Connect To Oracle | rb60db | bis | password | """ try: logger.debug( f'Connecting using : dbname={dbname}, dbusername={dbusername}, dbpassword={dbpassword}' ) connection_string = f'{dbusername}/{dbpassword}@{dbname}' self._connection = cx_Oracle.connect(connection_string) return self._cache.register(self.connection, alias) except cx_Oracle.DatabaseError as err: raise Exception("Logon to oracle Error:", str(err)) def disconnect_from_oracle(self) -> None: """ Close active Oracle connection. *Example:*\n | Connect To Oracle | rb60db | bis | password | | Disconnect From Oracle | """ self.connection.close() self._cache.empty_cache() def close_all_oracle_connections(self) -> None: """ Close all Oracle connections that were opened. You should not use [#Disconnect From Oracle|Disconnect From Oracle] and [#Close All Oracle Connections|Close All Oracle Connections] together. After calling this keyword connection IDs returned by opening new connections [#Connect To Oracle|Connect To Oracle], will start from 1. *Example:*\n | Connect To Oracle | rb60db | bis | password | alias=bis | | Connect To Oracle | rb60db | bis_dcs | password | alias=bis_dsc | | Switch Oracle Connection | bis | | @{sql_out_bis}= | Execute Sql String | select SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') from dual | | Switch Oracle Connection | bis_dsc | | @{sql_out_bis_dsc}= | Execute Sql String | select SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') from dual | | Close All Oracle Connections | """ self._connection = self._cache.close_all() def switch_oracle_connection(self, index_or_alias: Union[int, str]) -> int: """ Switch between existing Oracle connections using their connection IDs or aliases. The connection ID is obtained on creating connection. Connection alias is optional and can be set at connecting to DB [#Connect To Oracle|Connect To Oracle]. *Args:*\n _index_or_alias_ - connection ID or alias assigned to connection; *Returns:*\n ID of the previous connection. *Example:* (switch by alias)\n | Connect To Oracle | rb60db | bis | password | alias=bis | | Connect To Oracle | rb60db | bis_dcs | password | alias=bis_dsc | | Switch Oracle Connection | bis | | @{sql_out_bis}= | Execute Sql String | select SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') from dual | | Switch Oracle Connection | bis_dsc | | @{sql_out_bis_dsc}= | Execute Sql String | select SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') from dual | | Close All Oracle Connections | =>\n @{sql_out_bis} = BIS\n @{sql_out_bis_dcs}= BIS_DCS *Example:* (switch by index)\n | ${bis_index}= | Connect To Oracle | rb60db | bis | password | | ${bis_dcs_index}= | Connect To Oracle | rb60db | bis_dcs | password | | @{sql_out_bis_dcs_1}= | Execute Sql String | select SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') from dual | | ${previous_index}= | Switch Oracle Connection | ${bis_index} | | @{sql_out_bis}= | Execute Sql String | select SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') from dual | | Switch Oracle Connection | ${previous_index} | | @{sql_out_bis_dcs_2}= | Execute Sql String | select SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') from dual | | Close All Oracle Connections | =>\n ${bis_index}= 1\n ${bis_dcs_index}= 2\n @{sql_out_bis_dcs_1} = BIS_DCS\n ${previous_index}= 2\n @{sql_out_bis} = BIS\n @{sql_out_bis_dcs_2}= BIS_DCS """ old_index = self._cache.current_index self._connection = self._cache.switch(index_or_alias) return old_index @staticmethod def wrap_into_html_details(statement: str, summary: str) -> str: """Format statement for html logging. *Args:*\n _statement_: statement to log. _summary_: summary for details tag. *Returns:*\n Formatted statement. """ statement = sqlparse.format(statement, reindent=True, indent_width=4, keyword_case='upper') statement_html = escape(statement) data = f'<details><summary>{summary}</summary><p>{statement_html}</p></details>' return data def _execute_sql(self, cursor: cx_Oracle.Cursor, statement: str, params: Dict[str, Any]) -> cx_Oracle.Cursor: """ Execute SQL query on Oracle DB using active connection. *Args*:\n _cursor_: cursor object.\n _statement_: SQL query to be executed.\n _params_: SQL query parameters.\n *Returns:*\n Query results. """ statement_with_params = self._replace_parameters_in_statement( statement, params) _connection_info = '@'.join( (cursor.connection.username, cursor.connection.dsn)) data = self.wrap_into_html_details( statement=statement_with_params, summary=f'Executed PL/SQL statement on {_connection_info}') logger.info(data, html=True) cursor.prepare(statement) self.last_executed_statement = self._replace_parameters_in_statement( statement, params) self.last_used_connection_index = self._cache.current_index cursor.execute(None, params) @staticmethod def _get_timeout_from_execution_context() -> float: """Get timeout from Robot Framework execution context. Returns: Current timeout value in seconds or None if timeout is not set. """ timeouts = {} default_timeout = OracleDB.DEFAULT_TIMEOUT for timeout in EXECUTION_CONTEXTS.current.timeouts: if timeout.active: timeouts[timeout.type] = timeout.time_left() if timeouts.get(KeywordTimeout.type, None): return timeouts[KeywordTimeout.type] test_timeout = timeouts.get(TestTimeout.type, None) return test_timeout if test_timeout and test_timeout else default_timeout def _replace_parameters_in_statement(self, statement: str, params: Dict[str, Any]) -> str: """Update SQL query parameters, if any exist, with their values for logging purposes. *Args*:\n _statement_: SQL query to be updated.\n _params_: SQL query parameters.\n *Returns:*\n SQL query with parameter names replaced with their values. """ params_keys = sorted(params.keys(), reverse=True) for key in params_keys: if isinstance(params[key], (int, float)): statement = statement.replace(f':{key}', str(params[key])) elif params[key] is None: statement = statement.replace(f':{key}', 'NULL') else: statement = statement.replace(f':{key}', f"'{params[key]}'") return statement def execute_plsql_block(self, plsqlstatement: str, **params: Any) -> None: """ PL/SQL block execution. *Args:*\n _plsqlstatement_ - PL/SQL block;\n _params_ - PL/SQL block parameters;\n *Raises:*\n PLSQL Error: Error message encoded according to DB where the code was run *Returns:*\n PL/SQL block execution result. *Example:*\n | *Settings* | *Value* | | Library | OracleDB | | *Variables* | *Value* | | ${var_failed} | 3 | | *Test Cases* | *Action* | *Argument* | *Argument* | *Argument* | | Simple | | | ${statement}= | catenate | SEPARATOR=\\r\\n | DECLARE | | | ... | | | a NUMBER := ${var_failed}; | | | ... | | | BEGIN | | | ... | | | a := a + 1; | | | ... | | | if a = 4 then | | | ... | | | raise_application_error ( -20001, 'This is a custom error' ); | | | ... | | | end if; | | | ... | | | END; | | | Execute Plsql Block | plsqlstatement=${statement} | =>\n DatabaseError: ORA-20001: This is a custom error | | ${statement}= | catenate | SEPARATOR=\\r\\n | DECLARE | | | ... | | | a NUMBER := :var; | | | ... | | | BEGIN | | | ... | | | a := a + 1; | | | ... | | | if a = 4 then | | | ... | | | raise_application_error ( -20001, 'This is a custom error' ); | | | ... | | | end if; | | | ... | | | END; | | | Execute Plsql Block | plsqlstatement=${statement} | var=${var_failed} | =>\n DatabaseError: ORA-20001: This is a custom error """ cursor = self.connection.cursor() with sql_timeout(timeout=self._get_timeout_from_execution_context(), connection=cursor.connection): try: self._execute_sql(cursor, plsqlstatement, params) self.connection.commit() finally: self.connection.rollback() def execute_plsql_block_with_dbms_output(self, plsqlstatement: str, **params: Any) -> List[str]: """ Execute PL/SQL block with dbms_output(). *Args:*\n _plsqlstatement_ - PL/SQL block;\n _params_ - PL/SQL block parameters;\n *Raises:*\n PLSQL Error: Error message encoded according to DB where the code was run. *Returns:*\n List of values returned by Oracle dbms_output.put_line(). *Example:*\n | *Settings* | *Value* | | Library | OracleDB | | *Variables* | *Value* | | ${var} | 4 | | *Test Cases* | *Action* | *Argument* | *Argument* | *Argument* | | Simple | | | ${statement}= | catenate | SEPARATOR=\\r\\n | DECLARE | | | ... | | | a NUMBER := ${var}; | | | ... | | | BEGIN | | | ... | | | a := a + 1; | | | ... | | | if a = 4 then | | | ... | | | raise_application_error ( -20001, 'This is a custom error' ); | | | ... | | | end if; | | | ... | | | dbms_output.put_line ('text '||a||', e-mail text'); | | | ... | | | dbms_output.put_line ('string 2 '); | | | ... | | | END; | | | @{dbms}= | Execute Plsql Block With Dbms Output | plsqlstatement=${statement} | =>\n | @{dbms} | text 5, e-mail text | | | string 2 | | | ${statement}= | catenate | SEPARATOR=\\r\\n | DECLARE | | | ... | | | a NUMBER := :var; | | | ... | | | BEGIN | | | ... | | | a := a + 1; | | | ... | | | if a = 4 then | | | ... | | | raise_application_error ( -20001, 'This is a custom error' ); | | | ... | | | end if; | | | ... | | | dbms_output.put_line ('text '||a||', e-mail text'); | | | ... | | | dbms_output.put_line ('string 2 '); | | | ... | | | END; | | | @{dbms}= | Execute Plsql Block With Dbms Output | plsqlstatement=${statement} | var=${var} | =>\n | @{dbms} | text 5, e-mail text | | | string 2 | """ dbms_output = [] cursor = self.connection.cursor() with sql_timeout(timeout=self._get_timeout_from_execution_context(), connection=cursor.connection): try: cursor.callproc("dbms_output.enable") self._execute_sql(cursor, plsqlstatement, params) self.connection.commit() statusvar = cursor.var(cx_Oracle.NUMBER) linevar = cursor.var(cx_Oracle.STRING) while True: cursor.callproc("dbms_output.get_line", (linevar, statusvar)) if statusvar.getvalue() != 0: break dbms_output.append(linevar.getvalue()) return dbms_output finally: self.connection.rollback() def execute_plsql_script(self, file_path: str, **params: Any) -> None: """ Execution of PL/SQL code from file. *Args:*\n _file_path_ - path to PL/SQL script file;\n _params_ - PL/SQL code parameters;\n *Raises:*\n PLSQL Error: Error message encoded according to DB where the code was run. *Example:*\n | Execute Plsql Script | ${CURDIR}${/}plsql_script.sql | | Execute Plsql Script | ${CURDIR}${/}plsql_script.sql | first_param=1 | second_param=2 | """ with open(file_path, "r") as script: data = script.read() self.execute_plsql_block(data, **params) def execute_sql_string(self, plsqlstatement: str, **params: Any) -> List[Tuple[Any, ...]]: """ Execute PL/SQL string. *Args:*\n _plsqlstatement_ - PL/SQL string;\n _params_ - PL/SQL string parameters;\n *Raises:*\n PLSQL Error: Error message encoded according to DB where the code was run. *Returns:*\n PL/SQL string execution result. *Example:*\n | @{query}= | Execute Sql String | select sysdate, sysdate+1 from dual | | Set Test Variable | ${sys_date} | ${query[0][0]} | | Set Test Variable | ${next_date} | ${query[0][1]} | | @{query}= | Execute Sql String | select sysdate, sysdate+:d from dual | d=1 | | Set Test Variable | ${sys_date} | ${query[0][0]} | | Set Test Variable | ${next_date} | ${query[0][1]} | """ cursor = self.connection.cursor() with sql_timeout(timeout=self._get_timeout_from_execution_context(), connection=cursor.connection): try: self._execute_sql(cursor, plsqlstatement, params) query_result = cursor.fetchall() self.result_logger(query_result) return query_result finally: self.connection.rollback() def execute_sql_string_mapped(self, sql_statement: str, **params: Any) -> List[Dict[str, Any]]: """SQL query execution where each result row is mapped as a dict with column names as keys. *Args:*\n _sql_statement_ - PL/SQL string;\n _params_ - PL/SQL string parameters;\n *Returns:*\n A list of dictionaries where column names are mapped as keys. *Example:*\n | @{query}= | Execute Sql String Mapped| select sysdate, sysdate+1 from dual | | Set Test Variable | ${sys_date} | ${query[0][sysdate]} | | Set Test Variable | ${next_date} | ${query[0][sysdate1]} | | @{query}= | Execute Sql String Mapped| select sysdate, sysdate+:d from dual | d=1 | | Set Test Variable | ${sys_date} | ${query[0][sysdate]} | | Set Test Variable | ${next_date} | ${query[0][sysdate1]} | """ cursor = self.connection.cursor() with sql_timeout(timeout=self._get_timeout_from_execution_context(), connection=cursor.connection): try: self._execute_sql(cursor, sql_statement, params) col_name = tuple(i[0] for i in cursor.description) query_result = [dict(zip(col_name, row)) for row in cursor] self.result_logger(query_result) return query_result finally: self.connection.rollback() def execute_sql_string_generator( self, sql_statement: str, **params: Any) -> Iterable[Dict[str, Any]]: """Generator that yields each result row mapped as a dict with column names as keys.\n Intended for use mainly in code for other keywords. *If used, the generator must be explicitly closed before closing DB connection* *Args:*\n _sql_statement_ - PL/SQL string;\n _params_ - PL/SQL string parameters;\n Yields:*\n results dict. """ self.last_executed_statement = sql_statement self.last_executed_statement_params = params cursor = self.connection.cursor() with sql_timeout(timeout=self._get_timeout_from_execution_context(), connection=cursor.connection): try: self._execute_sql(cursor, sql_statement, params) col_name = tuple(i[0] for i in cursor.description) for row in cursor: yield dict(zip(col_name, row)) finally: self.connection.rollback() def result_logger(self, query_result: List[Any], result_amount: int = 10) -> None: """Log first n rows from the query results *Args:*\n _query_result_ - query result to log, must be greater than 0 _result_amount_ - amount of entries to display from result """ if len(query_result) > result_amount > 0: query_result = query_result[:result_amount] logged_result = self.wrap_into_html_details(str(query_result), "SQL Query Result") logger.info(logged_result, html=True) @contextmanager def use_connection(self, conn_index: Union[int, str]) -> Iterator[None]: """Context manager for switching connection. Args: conn_index: Connection index or alias to switch. Yields: generator. """ _old_con_index = self.switch_oracle_connection(conn_index) yield self.switch_oracle_connection(_old_con_index)
class Process(object): """Robot Framework test library for running processes. This library utilizes Python's [http://docs.python.org/2/library/subprocess.html|subprocess] module and its [http://docs.python.org/2/library/subprocess.html#subprocess.Popen|Popen] class. The library has following main usages: - Running processes in system and waiting for their completion using `Run Process` keyword. - Starting processes on background using `Start Process`. - Waiting started process to complete using `Wait For Process` or stopping them with `Terminate Process` or `Terminate All Processes`. This library is new in Robot Framework 2.8. == Table of contents == - `Specifying command and arguments` - `Process configuration` - `Active process` - `Result object` - `Boolean arguments` - `Example` - `Shortcuts` - `Keywords` = Specifying command and arguments = Both `Run Process` and `Start Process` accept the command to execute and all arguments passed to the command as separate arguments. This makes usage convenient and also allows these keywords to automatically escape possible spaces and other special characters in commands and arguments. Notice that if a command accepts options that themselves accept values, these options and their values must be given as separate arguments. When `running processes in shell`, it is also possible to give the whole command to execute as a single string. The command can then contain multiple commands to be run together. When using this approach, the caller is responsible on escaping. Examples: | `Run Process` | ${tools}${/}prog.py | argument | second arg with spaces | | `Run Process` | java | -jar | ${jars}${/}example.jar | --option | value | | `Run Process` | prog.py "one arg" && tool.sh | shell=yes | cwd=${tools} | Starting from Robot Framework 2.8.6, possible non-string arguments are converted to strings automatically. = Process configuration = `Run Process` and `Start Process` keywords can be configured using optional ``**configuration`` keyword arguments. Configuration arguments must be given after other arguments passed to these keywords and must use syntax like ``name=value``. Available configuration arguments are listed below and discussed further in sections afterwards. | = Name = | = Explanation = | | shell | Specifies whether to run the command in shell or not. | | cwd | Specifies the working directory. | | env | Specifies environment variables given to the process. | | env:<name> | Overrides the named environment variable(s) only. | | stdout | Path of a file where to write standard output. | | stderr | Path of a file where to write standard error. | | output_encoding | Encoding to use when reading command outputs. | | alias | Alias given to the process. | Note that because ``**configuration`` is passed using ``name=value`` syntax, possible equal signs in other arguments passed to `Run Process` and `Start Process` must be escaped with a backslash like ``name\\=value``. See `Run Process` for an example. == Running processes in shell == The ``shell`` argument specifies whether to run the process in a shell or not. By default shell is not used, which means that shell specific commands, like ``copy`` and ``dir`` on Windows, are not available. You can, however, run shell scripts and batch files without using a shell. Giving the ``shell`` argument any non-false value, such as ``shell=True``, changes the program to be executed in a shell. It allows using the shell capabilities, but can also make the process invocation operating system dependent. Having a shell between the actually started process and this library can also interfere communication with the process such as stopping it and reading its outputs. Because of these problems, it is recommended to use the shell only when absolutely necessary. When using a shell it is possible to give the whole command to execute as a single string. See `Specifying command and arguments` section for examples and more details in general. == Current working directory == By default the child process will be executed in the same directory as the parent process, the process running tests, is executed. This can be changed by giving an alternative location using the ``cwd`` argument. Forward slashes in the given path are automatically converted to backslashes on Windows. `Standard output and error streams`, when redirected to files, are also relative to the current working directory possibly set using the ``cwd`` argument. Example: | `Run Process` | prog.exe | cwd=${ROOT}/directory | stdout=stdout.txt | == Environment variables == By default the child process will get a copy of the parent process's environment variables. The ``env`` argument can be used to give the child a custom environment as a Python dictionary. If there is a need to specify only certain environment variable, it is possible to use the ``env:<name>=<value>`` format to set or override only that named variables. It is also possible to use these two approaches together. Examples: | `Run Process` | program | env=${environ} | | `Run Process` | program | env:http_proxy=10.144.1.10:8080 | env:PATH=%{PATH}${:}${PROGDIR} | | `Run Process` | program | env=${environ} | env:EXTRA=value | == Standard output and error streams == By default processes are run so that their standard output and standard error streams are kept in the memory. This works fine normally, but if there is a lot of output, the output buffers may get full and the program can hang. Additionally on Jython, everything written to these in-memory buffers can be lost if the process is terminated. To avoid the above mentioned problems, it is possible to use ``stdout`` and ``stderr`` arguments to specify files on the file system where to redirect the outputs. This can also be useful if other processes or other keywords need to read or manipulate the outputs somehow. Given ``stdout`` and ``stderr`` paths are relative to the `current working directory`. Forward slashes in the given paths are automatically converted to backslashes on Windows. As a special feature, it is possible to redirect the standard error to the standard output by using ``stderr=STDOUT``. Regardless are outputs redirected to files or not, they are accessible through the `result object` returned when the process ends. Commands are expected to write outputs using the console encoding, but `output encoding` can be configured using the ``output_encoding`` argument if needed. Examples: | ${result} = | `Run Process` | program | stdout=${TEMPDIR}/stdout.txt | stderr=${TEMPDIR}/stderr.txt | | `Log Many` | stdout: ${result.stdout} | stderr: ${result.stderr} | | ${result} = | `Run Process` | program | stderr=STDOUT | | `Log` | all output: ${result.stdout} | Note that the created output files are not automatically removed after the test run. The user is responsible to remove them if needed. == Output encoding == Executed commands are, by default, expected to write outputs to the `standard output and error streams` using the encoding used by the system console. If the command uses some other encoding, that can be configured using the ``output_encoding`` argument. This is especially useful on Windows where the console uses a different encoding than rest of the system, and many commands use the general system encoding instead of the console encoding. The value used with the ``output_encoding`` argument must be a valid encoding and must match the encoding actually used by the command. As a convenience, it is possible to use strings ``CONSOLE`` and ``SYSTEM`` to specify that the console or system encoding is used, respectively. If produced outputs use different encoding then configured, values got through the `result object` will be invalid. Examples: | `Start Process` | program | output_encoding=UTF-8 | | `Run Process` | program | stdout=${path} | output_encoding=SYSTEM | The support to set output encoding is new in Robot Framework 3.0. == Alias == A custom name given to the process that can be used when selecting the `active process`. Examples: | `Start Process` | program | alias=example | | `Run Process` | python | -c | print 'hello' | alias=hello | = Active process = The test library keeps record which of the started processes is currently active. By default it is latest process started with `Start Process`, but `Switch Process` can be used to select a different one. Using `Run Process` does not affect the active process. The keywords that operate on started processes will use the active process by default, but it is possible to explicitly select a different process using the ``handle`` argument. The handle can be the identifier returned by `Start Process` or an ``alias`` explicitly given to `Start Process` or `Run Process`. = Result object = `Run Process`, `Wait For Process` and `Terminate Process` keywords return a result object that contains information about the process execution as its attributes. The same result object, or some of its attributes, can also be get using `Get Process Result` keyword. Attributes available in the object are documented in the table below. | = Attribute = | = Explanation = | | rc | Return code of the process as an integer. | | stdout | Contents of the standard output stream. | | stderr | Contents of the standard error stream. | | stdout_path | Path where stdout was redirected or ``None`` if not redirected. | | stderr_path | Path where stderr was redirected or ``None`` if not redirected. | Example: | ${result} = | `Run Process` | program | | `Should Be Equal As Integers` | ${result.rc} | 0 | | `Should Match` | ${result.stdout} | Some t?xt* | | `Should Be Empty` | ${result.stderr} | | | ${stdout} = | `Get File` | ${result.stdout_path} | | `Should Be Equal` | ${stdout} | ${result.stdout} | | `File Should Be Empty` | ${result.stderr_path} | | = Boolean arguments = Some keywords accept arguments that are handled as Boolean values true or false. If such an argument is given as a string, it is considered false if it is either empty or case-insensitively equal to ``false`` or ``no``. Other strings are considered true regardless their value, and other argument types are tested using same [http://docs.python.org/2/library/stdtypes.html#truth-value-testing|rules as in Python]. True examples: | `Terminate Process` | kill=True | # Strings are generally true. | | `Terminate Process` | kill=yes | # Same as the above. | | `Terminate Process` | kill=${TRUE} | # Python ``True`` is true. | | `Terminate Process` | kill=${42} | # Numbers other than 0 are true. | False examples: | `Terminate Process` | kill=False | # String ``false`` is false. | | `Terminate Process` | kill=no | # Also string ``no`` is false. | | `Terminate Process` | kill=${EMPTY} | # Empty string is false. | | `Terminate Process` | kill=${FALSE} | # Python ``False`` is false. | Note that prior to Robot Framework 2.8 all non-empty strings, including ``false``, were considered true. Additionally, ``no`` is considered false only in Robot Framework 2.9 and newer. = Example = | ***** Settings ***** | Library Process | Suite Teardown `Terminate All Processes` kill=True | | ***** Test Cases ***** | Example | `Start Process` program arg1 arg2 alias=First | ${handle} = `Start Process` command.sh arg | command2.sh shell=True cwd=/path | ${result} = `Run Process` ${CURDIR}/script.py | `Should Not Contain` ${result.stdout} FAIL | `Terminate Process` ${handle} | ${result} = `Wait For Process` First | `Should Be Equal As Integers` ${result.rc} 0 """ ROBOT_LIBRARY_SCOPE = 'GLOBAL' ROBOT_LIBRARY_VERSION = get_version() TERMINATE_TIMEOUT = 30 KILL_TIMEOUT = 10 def __init__(self): self._processes = ConnectionCache('No active process.') self._results = {} def run_process(self, command, *arguments, **configuration): """Runs a process and waits for it to complete. ``command`` and ``*arguments`` specify the command to execute and arguments passed to it. See `Specifying command and arguments` for more details. ``**configuration`` contains additional configuration related to starting processes and waiting for them to finish. See `Process configuration` for more details about configuration related to starting processes. Configuration related to waiting for processes consists of ``timeout`` and ``on_timeout`` arguments that have same semantics as with `Wait For Process` keyword. By default there is no timeout, and if timeout is defined the default action on timeout is ``terminate``. Returns a `result object` containing information about the execution. Note that possible equal signs in ``*arguments`` must be escaped with a backslash (e.g. ``name\\=value``) to avoid them to be passed in as ``**configuration``. Examples: | ${result} = | Run Process | python | -c | print 'Hello, world!' | | Should Be Equal | ${result.stdout} | Hello, world! | | ${result} = | Run Process | ${command} | stderr=STDOUT | timeout=10s | | ${result} = | Run Process | ${command} | timeout=1min | on_timeout=continue | | ${result} = | Run Process | java -Dname\\=value Example | shell=True | cwd=${EXAMPLE} | This keyword does not change the `active process`. ``timeout`` and ``on_timeout`` arguments are new in Robot Framework 2.8.4. """ current = self._processes.current timeout = configuration.pop('timeout', None) on_timeout = configuration.pop('on_timeout', 'terminate') try: handle = self.start_process(command, *arguments, **configuration) return self.wait_for_process(handle, timeout, on_timeout) finally: self._processes.current = current def start_process(self, command, *arguments, **configuration): """Starts a new process on background. See `Specifying command and arguments` and `Process configuration` for more information about the arguments, and `Run Process` keyword for related examples. Makes the started process new `active process`. Returns an identifier that can be used as a handle to activate the started process if needed. Starting from Robot Framework 2.8.5, processes are started so that they create a new process group. This allows sending signals to and terminating also possible child processes. This is not supported by Jython in general nor by Python versions prior to 2.7 on Windows. """ conf = ProcessConfiguration(**configuration) command = conf.get_command(command, list(arguments)) self._log_start(command, conf) process = subprocess.Popen(command, **conf.popen_config) self._results[process] = ExecutionResult(process, **conf.result_config) return self._processes.register(process, alias=conf.alias) def _log_start(self, command, config): if is_list_like(command): command = self.join_command_line(command) logger.info(u'Starting process:\n%s' % system_decode(command)) logger.debug(u'Process configuration:\n%s' % config) def is_process_running(self, handle=None): """Checks is the process running or not. If ``handle`` is not given, uses the current `active process`. Returns ``True`` if the process is still running and ``False`` otherwise. """ return self._processes[handle].poll() is None def process_should_be_running(self, handle=None, error_message='Process is not running.'): """Verifies that the process is running. If ``handle`` is not given, uses the current `active process`. Fails if the process has stopped. """ if not self.is_process_running(handle): raise AssertionError(error_message) def process_should_be_stopped(self, handle=None, error_message='Process is running.'): """Verifies that the process is not running. If ``handle`` is not given, uses the current `active process`. Fails if the process is still running. """ if self.is_process_running(handle): raise AssertionError(error_message) def wait_for_process(self, handle=None, timeout=None, on_timeout='continue'): """Waits for the process to complete or to reach the given timeout. The process to wait for must have been started earlier with `Start Process`. If ``handle`` is not given, uses the current `active process`. ``timeout`` defines the maximum time to wait for the process. It can be given in [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#time-format| various time formats] supported by Robot Framework, for example, ``42``, ``42 s``, or ``1 minute 30 seconds``. ``on_timeout`` defines what to do if the timeout occurs. Possible values and corresponding actions are explained in the table below. Notice that reaching the timeout never fails the test. | = Value = | = Action = | | continue | The process is left running (default). | | terminate | The process is gracefully terminated. | | kill | The process is forcefully stopped. | See `Terminate Process` keyword for more details how processes are terminated and killed. If the process ends before the timeout or it is terminated or killed, this keyword returns a `result object` containing information about the execution. If the process is left running, Python ``None`` is returned instead. Examples: | # Process ends cleanly | | | | ${result} = | Wait For Process | example | | Process Should Be Stopped | example | | | Should Be Equal As Integers | ${result.rc} | 0 | | # Process does not end | | | | ${result} = | Wait For Process | timeout=42 secs | | Process Should Be Running | | | | Should Be Equal | ${result} | ${NONE} | | # Kill non-ending process | | | | ${result} = | Wait For Process | timeout=1min 30s | on_timeout=kill | | Process Should Be Stopped | | | | Should Be Equal As Integers | ${result.rc} | -9 | ``timeout`` and ``on_timeout`` are new in Robot Framework 2.8.2. """ process = self._processes[handle] logger.info('Waiting for process to complete.') if timeout: timeout = timestr_to_secs(timeout) if not self._process_is_stopped(process, timeout): logger.info('Process did not complete in %s.' % secs_to_timestr(timeout)) return self._manage_process_timeout(handle, on_timeout.lower()) return self._wait(process) def _manage_process_timeout(self, handle, on_timeout): if on_timeout == 'terminate': return self.terminate_process(handle) elif on_timeout == 'kill': return self.terminate_process(handle, kill=True) else: logger.info('Leaving process intact.') return None def _wait(self, process): result = self._results[process] result.rc = process.wait() or 0 result.close_streams() logger.info('Process completed.') return result def terminate_process(self, handle=None, kill=False): """Stops the process gracefully or forcefully. If ``handle`` is not given, uses the current `active process`. By default first tries to stop the process gracefully. If the process does not stop in 30 seconds, or ``kill`` argument is given a true value, (see `Boolean arguments`) kills the process forcefully. Stops also all the child processes of the originally started process. Waits for the process to stop after terminating it. Returns a `result object` containing information about the execution similarly as `Wait For Process`. On Unix-like machines graceful termination is done using ``TERM (15)`` signal and killing using ``KILL (9)``. Use `Send Signal To Process` instead if you just want to send either of these signals without waiting for the process to stop. On Windows graceful termination is done using ``CTRL_BREAK_EVENT`` event and killing using Win32 API function ``TerminateProcess()``. Examples: | ${result} = | Terminate Process | | | Should Be Equal As Integers | ${result.rc} | -15 | # On Unixes | | Terminate Process | myproc | kill=true | Limitations: - Graceful termination is not supported on Windows by Jython nor by Python versions prior to 2.7. Process is killed instead. - Stopping the whole process group is not supported by Jython at all nor by Python versions prior to 2.7 on Windows. - On Windows forceful kill only stops the main process, not possible child processes. Automatically killing the process if termination fails as well as returning a result object are new features in Robot Framework 2.8.2. Terminating also possible child processes, including using ``CTRL_BREAK_EVENT`` on Windows, is new in Robot Framework 2.8.5. """ process = self._processes[handle] if not hasattr(process, 'terminate'): raise RuntimeError('Terminating processes is not supported ' 'by this Python version.') terminator = self._kill if is_truthy(kill) else self._terminate try: terminator(process) except OSError: if not self._process_is_stopped(process, self.KILL_TIMEOUT): raise logger.debug('Ignored OSError because process was stopped.') return self._wait(process) def _kill(self, process): logger.info('Forcefully killing process.') if hasattr(os, 'killpg'): os.killpg(process.pid, signal_module.SIGKILL) else: process.kill() if not self._process_is_stopped(process, self.KILL_TIMEOUT): raise RuntimeError('Failed to kill process.') def _terminate(self, process): logger.info('Gracefully terminating process.') # Sends signal to the whole process group both on POSIX and on Windows # if supported by the interpreter. if hasattr(os, 'killpg'): os.killpg(process.pid, signal_module.SIGTERM) elif hasattr(signal_module, 'CTRL_BREAK_EVENT'): if IRONPYTHON: # https://ironpython.codeplex.com/workitem/35020 ctypes.windll.kernel32.GenerateConsoleCtrlEvent( signal_module.CTRL_BREAK_EVENT, process.pid) else: process.send_signal(signal_module.CTRL_BREAK_EVENT) else: process.terminate() if not self._process_is_stopped(process, self.TERMINATE_TIMEOUT): logger.info('Graceful termination failed.') self._kill(process) def terminate_all_processes(self, kill=False): """Terminates all still running processes started by this library. This keyword can be used in suite teardown or elsewhere to make sure that all processes are stopped, By default tries to terminate processes gracefully, but can be configured to forcefully kill them immediately. See `Terminate Process` that this keyword uses internally for more details. """ for handle in range(1, len(self._processes) + 1): if self.is_process_running(handle): self.terminate_process(handle, kill=kill) self.__init__() def send_signal_to_process(self, signal, handle=None, group=False): """Sends the given ``signal`` to the specified process. If ``handle`` is not given, uses the current `active process`. Signal can be specified either as an integer as a signal name. In the latter case it is possible to give the name both with or without ``SIG`` prefix, but names are case-sensitive. For example, all the examples below send signal ``INT (2)``: | Send Signal To Process | 2 | | # Send to active process | | Send Signal To Process | INT | | | | Send Signal To Process | SIGINT | myproc | # Send to named process | This keyword is only supported on Unix-like machines, not on Windows. What signals are supported depends on the system. For a list of existing signals on your system, see the Unix man pages related to signal handling (typically ``man signal`` or ``man 7 signal``). By default sends the signal only to the parent process, not to possible child processes started by it. Notice that when `running processes in shell`, the shell is the parent process and it depends on the system does the shell propagate the signal to the actual started process. To send the signal to the whole process group, ``group`` argument can be set to any true value (see `Boolean arguments`). This is not supported by Jython, however. New in Robot Framework 2.8.2. Support for ``group`` argument is new in Robot Framework 2.8.5. """ if os.sep == '\\': raise RuntimeError('This keyword does not work on Windows.') process = self._processes[handle] signum = self._get_signal_number(signal) logger.info('Sending signal %s (%d).' % (signal, signum)) if is_truthy(group) and hasattr(os, 'killpg'): os.killpg(process.pid, signum) elif hasattr(process, 'send_signal'): process.send_signal(signum) else: raise RuntimeError('Sending signals is not supported ' 'by this Python version.') def _get_signal_number(self, int_or_name): try: return int(int_or_name) except ValueError: return self._convert_signal_name_to_number(int_or_name) def _convert_signal_name_to_number(self, name): try: return getattr(signal_module, name if name.startswith('SIG') else 'SIG' + name) except AttributeError: raise RuntimeError("Unsupported signal '%s'." % name) def get_process_id(self, handle=None): """Returns the process ID (pid) of the process as an integer. If ``handle`` is not given, uses the current `active process`. Notice that the pid is not the same as the handle returned by `Start Process` that is used internally by this library. """ return self._processes[handle].pid def get_process_object(self, handle=None): """Return the underlying ``subprocess.Popen`` object. If ``handle`` is not given, uses the current `active process`. """ return self._processes[handle] def get_process_result(self, handle=None, rc=False, stdout=False, stderr=False, stdout_path=False, stderr_path=False): """Returns the specified `result object` or some of its attributes. The given ``handle`` specifies the process whose results should be returned. If no ``handle`` is given, results of the current `active process` are returned. In either case, the process must have been finishes before this keyword can be used. In practice this means that processes started with `Start Process` must be finished either with `Wait For Process` or `Terminate Process` before using this keyword. If no other arguments than the optional ``handle`` are given, a whole `result object` is returned. If one or more of the other arguments are given any true value, only the specified attributes of the `result object` are returned. These attributes are always returned in the same order as arguments are specified in the keyword signature. See `Boolean arguments` section for more details about true and false values. Examples: | Run Process | python | -c | print 'Hello, world!' | alias=myproc | | # Get result object | | | | ${result} = | Get Process Result | myproc | | Should Be Equal | ${result.rc} | ${0} | | Should Be Equal | ${result.stdout} | Hello, world! | | Should Be Empty | ${result.stderr} | | | # Get one attribute | | | | ${stdout} = | Get Process Result | myproc | stdout=true | | Should Be Equal | ${stdout} | Hello, world! | | # Multiple attributes | | | | ${stdout} | ${stderr} = | Get Process Result | myproc | stdout=yes | stderr=yes | | Should Be Equal | ${stdout} | Hello, world! | | Should Be Empty | ${stderr} | | Although getting results of a previously executed process can be handy in general, the main use case for this keyword is returning results over the remote library interface. The remote interface does not support returning the whole result object, but individual attributes can be returned without problems. New in Robot Framework 2.8.2. """ result = self._results[self._processes[handle]] if result.rc is None: raise RuntimeError('Getting results of unfinished processes ' 'is not supported.') attributes = self._get_result_attributes(result, rc, stdout, stderr, stdout_path, stderr_path) if not attributes: return result elif len(attributes) == 1: return attributes[0] return attributes def _get_result_attributes(self, result, *includes): attributes = (result.rc, result.stdout, result.stderr, result.stdout_path, result.stderr_path) includes = (is_truthy(incl) for incl in includes) return tuple(attr for attr, incl in zip(attributes, includes) if incl) def switch_process(self, handle): """Makes the specified process the current `active process`. The handle can be an identifier returned by `Start Process` or the ``alias`` given to it explicitly. Example: | Start Process | prog1 | alias=process1 | | Start Process | prog2 | alias=process2 | | # currently active process is process2 | | Switch Process | process1 | | # now active process is process1 | """ self._processes.switch(handle) def _process_is_stopped(self, process, timeout): stopped = lambda: process.poll() is not None max_time = time.time() + timeout while time.time() <= max_time and not stopped(): time.sleep(min(0.1, timeout)) return stopped() def split_command_line(self, args, escaping=False): """Splits command line string into a list of arguments. String is split from spaces, but argument surrounded in quotes may contain spaces in them. If ``escaping`` is given a true value, then backslash is treated as an escape character. It can escape unquoted spaces, quotes inside quotes, and so on, but it also requires using double backslashes when using Windows paths. Examples: | @{cmd} = | Split Command Line | --option "value with spaces" | | Should Be True | $cmd == ['--option', 'value with spaces'] | New in Robot Framework 2.9.2. """ return cmdline2list(args, escaping=escaping) def join_command_line(self, *args): """Joins arguments into one command line string. In resulting command line string arguments are delimited with a space, arguments containing spaces are surrounded with quotes, and possible quotes are escaped with a backslash. If this keyword is given only one argument and that is a list like object, then the values of that list are joined instead. Example: | ${cmd} = | Join Command Line | --option | value with spaces | | Should Be Equal | ${cmd} | --option "value with spaces" | New in Robot Framework 2.9.2. """ if len(args) == 1 and is_list_like(args[0]): args = args[0] return subprocess.list2cmdline(args)
class CouchbaseManager(object): """ Library for managing Couchbase server. Based on: [ http://docs.couchbase.com/couchbase-manual-2.5/cb-rest-api/ | Using the REST API ] == Dependencies == | robot framework | http://robotframework.org | == Example == | *Settings* | *Value* | | Library | CouchbaseManager | | Library | Collections | | *Test Cases* | *Action* | *Argument* | *Argument* | *Argument* | *Argument* | *Argument* | | Simple | | | Connect To Couchbase | my_host_name | 8091 | administrator | administrator | alias=couchbase | | | ${overview}= | Overview | | | Log Dictionary | ${overview} | | | Close All Couchbase Connections | """ ROBOT_LIBRARY_SCOPE = 'GLOBAL' def __init__(self, pool: str = 'default') -> None: """ Library initialization.\n Robot Framework ConnectionCache() class is prepared for working with concurrent connections. *Args*:\n _pool_: name for connection pool. """ self._connection: Optional[RequestConnection] = None self.headers: Dict[str, str] = {} self._cache = ConnectionCache() self.pool = pool @property def connection(self) -> RequestConnection: """Check and return connection to Couchbase server. *Raises:*\n RuntimeError: if connection to Couchbase server hasn't been created yet. *Returns:*\n Current connection to Couchbase server. """ if self._connection is None: raise RuntimeError( 'There is no open connection to Couchbase server.') return self._connection def connect_to_couchbase(self, host: str, port: Union[int, str], username: str = 'administrator', password: str = 'administrator', timeout: Union[int, str] = 15, alias: str = None) -> int: """ Create connection to Couchbase server and set it as active connection. *Args:*\n _host_ - hostname;\n _port_ - port address;\n _username_ - username;\n _password_ - user password;\n _timeout_ - connection attempt timeout;\n _alias_ - connection alias;\n *Returns:*\n Index of the created connection. *Example:*\n | Connect To Couchbase | my_host_name | 8091 | administrator | administrator | alias=rmq | """ port = int(port) timeout = int(timeout) logger.debug( f'Connecting using : host={host}, port={port}, username={username},' f'password={password}, timeout={timeout}, alias={alias}') self._connection = RequestConnection(host, port, username, password, timeout) return self._cache.register(self.connection, alias) def switch_couchbase_connection(self, index_or_alias: Union[int, str]) -> int: """ Switch to another existing Couchbase connection using its index or alias.\n Connection alias is set in keyword [#Connect To Couchbase|Connect To Couchbase], which also returns connection index. *Args:*\n _index_or_alias_ - connection index or alias; *Returns:*\n Index of the previous connection. *Example:*\n | Connect To Couchbase | my_host_name_1 | 8091 | administrator | administrator | alias=couchbase1 | | Connect To Couchbase | my_host_name_2 | 8091 | administrator | administrator | alias=couchbase2 | | Switch Couchbase Connection | couchbase1 | | ${overview}= | Overview | | Switch Couchbase Connection | couchbase2 | | ${overview}= | Overview | | Close All Couchbase Connections | """ old_index = self._cache.current_index self._connection = self._cache.switch(index_or_alias) return old_index def disconnect_from_couchbase(self) -> None: """ Close active Couchbase connection. *Example:*\n | Connect To Couchbase | my_host_name | 8091 | administrator | administrator | alias=couchbase | | Disconnect From Couchbase | """ logger.debug( f'Close connection with : host={self.connection.host}, port={self.connection.port}' ) self.connection.close() def close_all_couchbase_connections(self) -> None: """ Close all open Couchbase connections.\n You should not use [#Disconnect From Couchbase|Disconnect From Couchbase] and [#Close All Couchbase Connections|Close All Couchbase Connections] together.\n After executing this keyword connection indexes returned by opening new connections [#Connect To Couchbase |Connect To Couchbase] starts from 1.\n *Example:*\n | Connect To Couchbase | my_host_name | 8091 | administrator | administrator | alias=couchbase | | Close All Couchbase Connections | """ self._connection = self._cache.close_all() def _prepare_request_headers(self, body: Any = None) -> Dict[str, str]: """ Prepare headers for HTTP request. Args:*\n _body_: HTTP request body.\n *Returns:*\n Dictionary with HTTP request headers\n """ headers = self.headers.copy() headers["Accept"] = "application/json" if body: headers["Content-Type"] = "application/x-www-form-urlencoded" return headers def overview(self) -> Dict[str, Any]: """ Get overview info on Couchbase server. *Returns:*\n Dictionary with overview info. *Raises:*\n raise HTTPError if the HTTP request returned an unsuccessful status code. *Example:*\n | ${overview}= | Overview | | Log Dictionary | ${overview} | | ${version}= | Get From Dictionary | ${overview} | implementationVersion | =>\n | ${version} = 2.2.0-821-rel-enterprise """ url = f'{self.connection.url}/pools/' response = requests.get(url, auth=self.connection.auth, headers=self._prepare_request_headers(), timeout=self.connection.timeout) response.raise_for_status() return response.json() def view_all_buckets(self) -> List[Dict[str, Any]]: """ Retrieve information on all buckets and their operations. *Returns:*\n List with buckets information *Raises:*\n raise HTTPError if the HTTP request returned an unsuccessful status code. *Example:*\n | ${buckets}= | View all buckets | default | | Log list | ${buckets} | =>\n | List length is 3 and it contains following items: | 0: {u'bucketType': u'membase', u'localRandomKeyUri' | ... """ url = f'{self.connection.url}/pools/{self.pool}/buckets' response = requests.get(url, auth=self.connection.auth, headers=self._prepare_request_headers(), timeout=self.connection.timeout) response.raise_for_status() return response.json() def get_names_of_all_buckets(self) -> List[str]: """ Retrieve all bucket names for active pool. *Returns:*\n List with bucket names. *Example:*\n | ${names}= | Get names of all buckets | default | | Log list | ${names} | =>\n | 0: default | 1: ufm | 2: ufm_support """ names = [] data = self.view_all_buckets() for item in data: names.append(item['name']) return names def flush_bucket(self, bucket: str) -> None: """ Flush specified bucket. *Args:*\n _bucket_ - bucket name;\n *Raises:*\n raise HTTPError if the HTTP request returned an unsuccessful status code. *Example:*\n | Flush bucket | default | """ url = f'{self.connection.url}/pools/{self.pool}/buckets/{bucket}/controller/doFlush' response = requests.post(url, auth=self.connection.auth, headers=self._prepare_request_headers(), timeout=self.connection.timeout) response.raise_for_status() def modify_bucket_parameters(self, bucket: str, **kwargs: Any) -> None: """ Modify bucket parameters. *Args:*\n _bucket_ - bucket name;\n _**kwargs_ - bucket parameters, parameter_name=value; parameter list can be found in [http://docs.couchbase.com/couchbase-manual-2.5/cb-rest-api/#modifying-bucket-parameters| Couchbase doc] *Raises:*\n raise HTTPError if the HTTP request returned an unsuccessful status code. *Example:*\n | Modify bucket parameters | default | flushEnabled=1 | ramQuotaMB=297 | """ url = f'{self.connection.url}/pools/{self.pool}/buckets/{bucket}' response = requests.post( url, auth=self.connection.auth, data=kwargs, headers=self._prepare_request_headers(body=kwargs), timeout=self.connection.timeout) response.raise_for_status()
class SessionManager(object): """Session manager keywords for DynamoDB operations.""" def __init__(self): self._builtin = BuiltIn() self._cache = ConnectionCache('No sessions.') def create_dynamodb_session(self, *args, **kwargs): # pylint: disable=line-too-long """Create DynamoDB session object. Arguments: - ``region``: The name of AWS region. - ``session``: The session object to AWS connection. (Optional) - ``profile``: The profile name to be use to create the session. (Optional) - ``access_key``: If ``session`` is None, use this access key to create the session. (Optional) - ``secret_key``: If ``session`` is None, use this secret key to create the session. (Optional) - ``session_token``: If ``session`` is None, use this session token to create the session. (Optional) - ``host``: The address of the host. Use this to connect to a local instance. (Optional) - ``port``: Connect to the host on this port. (Default 80) - ``is_secure``: Enforce https connection. (Default True) - ``label``: A case and space insensitive string to identify the DynamoDB session. (Default ``region``) Examples: | Create DynamoDB Session | | | | | # Use default config | | Create DynamoDB Session | us-west-1 | | | | # Use default profile | | Create DynamoDB Session | us-west-1 | profile=profile1 | | | # Use profile1 | | Create DynamoDB Session | us-west-1 | access_key=KEY | secret_key=SECRET | | # Label is us-west-1 | | Create DynamoDB Session | us-west-1 | access_key=KEY | secret_key=SECRET | label=LABEL | # Label is LABEL | """ # pylint: disable=line-too-long kargs = dict(enumerate(args)) region = kargs.get(0, kwargs.pop('region', None)) label = kwargs.pop('label', region) session = Engine() # pylint: disable=protected-access session._session = kwargs.pop('session', None) # pylint: disable=protected-access if session._session is None: # pylint: disable=protected-access session._session = self._get_session(region=region, **kwargs) # pylint: disable=protected-access client = self._get_client(session._session, region=region, **kwargs) kwargs.pop('access_key', None) kwargs.pop('host', None) kwargs.pop('is_secure', None) kwargs.pop('port', None) kwargs.pop('profile', None) kwargs.pop('secret_key', None) kwargs.pop('session_token', None) session.connection = DynamoDBConnection(client, **kwargs) if label is None: label = session.connection.region # pylint: disable=protected-access self._builtin.log('Creating DynamoDB session: %s' % label, 'DEBUG') self._cache.register(session, alias=label) return label def delete_all_dynamodb_sessions(self): """Removes all DynamoDB sessions.""" self._cache.empty_cache() def delete_dynamodb_session(self, label): """Removes DynamoDB session. Arguments: - ``label``: A case and space insensitive string to identify the DynamoDB session. (Default ``region``) Examples: | Delete DynamoDB Session | LABEL | """ self._cache.switch(label) index = self._cache.current_index # pylint: disable=protected-access self._cache.current = self._cache._no_current # pylint: disable=protected-access self._cache._connections[index - 1] = None # pylint: disable=protected-access self._cache._aliases['x-%s-x' % label] = self._cache._aliases.pop(label) def _get_client(self, session, **kwargs): """Returns boto3 client session object.""" client_kwargs = {} host = kwargs.pop('host', None) is_secure = kwargs.pop('is_secure', True) port = kwargs.pop('port', None) region = kwargs.pop('region', None) url = self._get_url(host, port, is_secure) if region is not None: client_kwargs['region_name'] = region if url is not None: client_kwargs['endpoint_url'] = url if not is_secure: client_kwargs['use_ssl'] = is_secure return session.client('dynamodb', **client_kwargs) @staticmethod def _get_session(**kwargs): """Returns boto3 session object.""" access_key = kwargs.pop('access_key', None) profile = kwargs.pop('profile', None) region = kwargs.pop('region', None) session_kwargs = {} token = kwargs.pop('session_token', None) if access_key is not None: session_kwargs['aws_access_key_id'] = access_key session_kwargs['aws_secret_access_key'] = kwargs.pop('secret_key', None) if profile is not None: session_kwargs['profile_name'] = profile if region is not None: session_kwargs['region_name'] = region if token is not None: session_kwargs['aws_session_token'] = token return Session(**session_kwargs) @staticmethod def _get_url(host, port, is_secure=True): """Returns pre-format host endpoint URL.""" url = None if host is not None: protocol = 'https' if is_secure else 'http' url = '%s://%s' % (protocol, host) if port is not None: url += ':%d' % int(port) return url
class RestKeywords(object): def __init__(self): self._cache = ConnectionCache() self._builtin = BuiltIn() @staticmethod def _normalize(d): if type(d) is dict: return dict((k, RestKeywords._normalize(v)) for k, v in d.iteritems() if v and RestKeywords._normalize(v)) elif type(d) is list: return [RestKeywords._normalize(v) for v in d if v and RestKeywords._normalize(v)] else: return d @staticmethod def convert_to_json(strings, normalize="False"): if type(strings) is list: json_ = map(lambda s: json.loads(s), strings) else: json_ = json.loads(strings) if normalize.upper() == "TRUE": if type(json_) is list: json_ = map(lambda x: RestKeywords._normalize(x), json_) else: json_ = RestKeywords._normalize(json_) return json_ @staticmethod def convert_to_multipart_encoded_files(files): mpe_files = [] for f in files: form_field_name = f[0] file_name = path.basename(f[1]) file_path = f[1] mime_type = f[2] mpe_files.append((form_field_name, (file_name, open(file_path, "rb"), mime_type))) return mpe_files def create_session(self, alias, headers=None, auth=None, verify="False", cert=None): session = Session() if headers: session.headers.update(headers) if auth: session.auth = tuple(auth) session.verify = self._builtin.convert_to_boolean(verify) session.cert = cert self._cache.register(session, alias) def head(self, alias, url, params=None, headers=None, cookies=None, timeout=10): logger.info("Sending HEAD request to: '" + url + "', session: '" + alias + "'") session = self._cache.switch(alias) resp = session.head(url, params=params, headers=headers, cookies=cookies, timeout=timeout) return {"status": resp.status_code, "headers": resp.headers} def get(self, alias, url, params=None, headers=None, cookies=None, timeout=10): logger.info("Sending GET request to: '" + url + "', session: '" + alias + "'") session = self._cache.switch(alias) resp = session.get(url, params=params, headers=headers, cookies=cookies, timeout=timeout) try: return {"status": resp.status_code, "headers": resp.headers, "body": resp.json()} except ValueError: return {"status": resp.status_code, "headers": resp.headers, "body": resp.content} def post(self, alias, url, headers=None, data=None, files=None, cookies=None, timeout=10): logger.info("Sending POST request to: '" + url + "', session: '" + alias + "'") session = self._cache.switch(alias) resp = session.post(url, headers=headers, cookies=cookies, data=data.encode("utf-8"), files=files, timeout=timeout) try: return {"status": resp.status_code, "headers": resp.headers, "body": resp.json()} except ValueError: return {"status": resp.status_code, "headers": resp.headers, "body": resp.content} def put(self, alias, url, headers=None, data=None, cookies=None, timeout=10): logger.info("Sending PUT request to: '" + url + "', session: '" + alias + "'") session = self._cache.switch(alias) resp = session.put(url, headers=headers, cookies=cookies, data=data.encode("utf-8"), timeout=timeout) try: return {"status": resp.status_code, "headers": resp.headers, "body": resp.json()} except ValueError: return {"status": resp.status_code, "headers": resp.headers, "body": resp.content} def delete(self, alias, url, headers=None, data=None, cookies=None, timeout=10): logger.info("Sending DELETE request to: '" + url + "', session: '" + alias + "'") session = self._cache.switch(alias) resp = session.delete(url, headers=headers, cookies=cookies, data=data.encode("utf-8"), timeout=timeout) try: return {"status": resp.status_code, "headers": resp.headers, "body": resp.json()} except ValueError: return {"status": resp.status_code, "headers": resp.headers, "body": resp.content} def close_all_sessions(self): self._cache.empty_cache()
class ConnectionManager(object): """ Class that handles connection/disconnection to databases. """ def __init__(self): self._connectionCache = ConnectionCache() def connect_to_database(self, driverName=None, dbName=None, username=None, password=None, host='localhost', port="5432", alias=None): """ Connects to database. *Arguments:* - driverName: string, name of python database driver. - dbName: string, name of database. - username: string, name of user. - password: string, user password. - host: string, database host. - port: int, database port. - alias: string, database alias for future use. *Return:* - None *Examples:* | Connect To Database | psycopg2 | PyDB | username | password \ | localhost | 5432 | SomeCompanyDB | """ if isinstance(driverName, str): dbModule = __import__(driverName) else: dbModule = driverName driverName = 'Mock DB Driver' connParams = {'database': dbName, 'user': username, 'password': password, 'host': host, 'port': port} if driverName in ("MySQLdb", "pymysql"): connParams = {'db': dbName, 'user': username, 'passwd': password, 'host': host, 'port': port} elif driverName in ("psycopg2"): connParams = {'database': dbName, 'user': username, 'password': password, 'host': host, 'port': port} connStr = ['%s: %s' % (k, str(connParams[k])) for k in connParams] logger.debug('Connect using: %s' % ', '.join(connStr)) dbConnection = _Connection(driverName, dbModule.connect(**connParams)) self._connectionCache.register(dbConnection, alias) logger.info("Established connection to the %s database. " "Alias %s. Driver name: %s." % (dbName, alias, driverName)) def disconnect_from_database(self): """ Disconnects from database. *Arguments:* - None *Return:* - None *Examples:* | Disconnect From Database | """ if self._connectionCache.current: self._connectionCache.current.close() curIndex = self._connectionCache.current_index aliasesCache = self._connectionCache._aliases if curIndex in aliasesCache.values(): keyForDeletion = \ aliasesCache.keys()[aliasesCache.values().index(curIndex)] del self._connectionCache._aliases[keyForDeletion] self._connectionCache.current = self._connectionCache._no_current logger.info("Current database was disconnected.") cls_attr = getattr(type(self._connectionCache), 'current_index', None) if isinstance(cls_attr, property) and cls_attr.fset is not None: self._connectionCache.current_index = None def disconnect_from_all_databases(self): """ Disconnects from all previously opened databases. *Arguments:* - None *Return:* - None *Examples:* | Disconnect From All Databases | """ self._connectionCache.close_all('close') logger.info("All databases were disconnected.") def set_current_database(self, aliasOrIndex): """ Sets current database by alias or index. *Arguments:* - aliasOrIndex: int or string, alias or index of opened database. *Return:* - None *Examples:* | # Using index | | Set Current Database | 1 | | # Using alias | | Set Current Database | SomeCompanyDB | """ self._connectionCache.switch(aliasOrIndex) logger.info("Connection switched to the %s database." % aliasOrIndex)
class WinRMLibrary(object): """ Robot Framework library for Windows Remote Management, based on pywinrm. == Enable Windows Remote Shell == - [ http://support.microsoft.com/kb/555966 | KB-555966 ] - Execute on windows server: | winrm set winrm/config/client/auth @{Basic="true"} | winrm set winrm/config/service/auth @{Basic="true"} | winrm set winrm/config/service @{AllowUnencrypted="true"} == Dependence == | pywinrm | https://pypi.python.org/pypi/pywinrm | | robot framework | http://robotframework.org | """ ROBOT_LIBRARY_SCOPE='GLOBAL' def __init__(self): self._session=None self._cache=ConnectionCache('No sessions created') def create_session (self, alias, hostname, login, password): """ Create session with windows host. Does not support domain authentification. *Args:*\n _alias_ - robot framework alias to identify the session\n _hostname_ - windows hostname (not IP)\n _login_ - windows local login\n _password_ - windows local password *Returns:*\n Session index *Example:*\n | Create Session | server | windows-host | Administrator | 1234567890 | """ logger.debug ('Connecting using : hostname=%s, login=%s, password=%s '%(hostname, login, password)) self._session=winrm.Session(hostname, (login, password)) return self._cache.register(self._session, alias) def run_cmd (self, alias, command, params=None): """ Execute command on remote mashine. *Args:*\n _alias_ - robot framework alias to identify the session\n _command_ - windows command\n _params_ - lists of command's parameters *Returns:*\n Result object with methods: status_code, std_out, std_err. *Example:*\n | ${params}= | Create List | "/all" | | ${result}= | Run cmd | server | ipconfig | ${params} | | Log | ${result.status_code} | | Log | ${result.std_out} | | Log | ${result.std_err} | =>\n | 0 | Windows IP Configuration | Host Name . . . . . . . . . . . . : WINDOWS-HOST | Primary Dns Suffix . . . . . . . : | Node Type . . . . . . . . . . . . : Hybrid | IP Routing Enabled. . . . . . . . : No | WINS Proxy Enabled. . . . . . . . : No | """ if params is not None: log_cmd=command+' '+' '.join(params) else: log_cmd=command logger.info ('Run command on server with alias "%s": %s '%(alias, log_cmd)) self._session=self._cache.switch(alias) result=self._session.run_cmd (command, params) return result def run_ps (self, alias, script): """ Run power shell script on remote mashine. *Args:*\n _alias_ - robot framework alias to identify the session\n _script_ - power shell script\n *Returns:*\n Result object with methods: status_code, std_out, std_err. *Example:*\n | ${result}= | Run ps | server | get-process iexplore|select -exp ws|measure-object -sum|select -exp Sum | | Log | ${result.status_code} | | Log | ${result.std_out} | | Log | ${result.std_err} | =>\n | 0 | 56987648 | """ logger.info ('Run power shell script on server with alias "%s": %s '%(alias, script)) self._session=self._cache.switch(alias) result=self._session.run_ps (script) return result def delete_all_sessions(self): """ Removes all sessions with windows hosts""" self._cache.empty_cache()
class Process(object): """Robot Framework test library for running processes. This library utilizes Python's [http://docs.python.org/2.7/library/subprocess.html|subprocess] module and its [http://docs.python.org/2.7/library/subprocess.html#subprocess.Popen|Popen] class. The library has following main usages: - Running processes in system and waiting for their completion using `Run Process` keyword. - Starting processes on background using `Start Process`. - Waiting started process to complete using `Wait For Process` or stopping them with `Terminate Process` or `Terminate All Processes`. This library is new in Robot Framework 2.8. == Table of contents == - `Specifying command and arguments` - `Process configuration` - `Active process` - `Stopping processes` - `Result object` - `Using with OperatingSystem library` - `Example` = Specifying command and arguments = Both `Run Process` and `Start Process` accept the command to execute and all arguments passed to it as separate arguments. This is convenient to use and also allows these keywords to automatically escape possible spaces and other special characters in the command or arguments. When `running processes in shell`, it is also possible to give the whole command to execute as a single string. The command can then contain multiple commands, for example, connected with pipes. When using this approach the caller is responsible on escaping. Examples: | `Run Process` | ${progdir}${/}prog.py | first arg | second | | `Run Process` | script1.sh arg && script2.sh | shell=yes | cwd=${progdir} | = Process configuration = `Run Process` and `Start Process` keywords can be configured using optional `**configuration` keyword arguments. Available configuration arguments are listed below and discussed further in sections afterwards. | *Name* | *Explanation* | | shell | Specifies whether to run the command in shell or not | | cwd | Specifies the working directory. | | env | Specifies environment variables given to the process. | | env:<name> | Overrides the named environment variable(s) only. | | stdout | Path of a file where to write standard output. | | stderr | Path of a file where to write standard error. | | alias | Alias given to the process. | Configuration must be given after other arguments passed to these keywords and must use syntax `name=value`. == Running processes in shell == The `shell` argument specifies whether to run the process in a shell or not. By default shell is not used, which means that shell specific commands, like `copy` and `dir` on Windows, are not available. Giving the `shell` argument any non-false value, such as `shell=True`, changes the program to be executed in a shell. It allows using the shell capabilities, but can also make the process invocation operating system dependent. When using a shell it is possible to give the whole command to execute as a single string. See `Specifying command and arguments` section for more details. == Current working directory == By default the child process will be executed in the same directory as the parent process, the process running tests, is executed. This can be changed by giving an alternative location using the `cwd` argument. Forward slashes in the given path are automatically converted to backslashes on Windows. `Standard output and error streams`, when redirected to files, are also relative to the current working directory possibly set using the `cwd` argument. Example: | `Run Process` | prog.exe | cwd=${ROOT}/directory | stdout=stdout.txt | == Environment variables == By default the child process will get a copy of the parent process's environment variables. The `env` argument can be used to give the child a custom environment as a Python dictionary. If there is a need to specify only certain environment variable, it is possible to use the `env:<name>` format to set or override only that named variables. It is also possible to use these two approaches together. Examples: | `Run Process` | program | env=${environ} | | `Run Process` | program | env:PATH=%{PATH}${:}${PROGRAM DIR} | | `Run Process` | program | env=${environ} | env:EXTRA=value | == Standard output and error streams == By default processes are run so that their standard output and standard error streams are kept in the memory. This works fine normally, but if there is a lot of output, the output buffers may get full and the program could hang. To avoid output buffers getting full, it is possible to use `stdout` and `stderr` arguments to specify files on the file system where to redirect the outputs. This can also be useful if other processes or other keywords need to read or manipulate the outputs somehow. Given `stdout` and `stderr` paths are relative to the `current working directory`. Forward slashes in the given paths are automatically converted to backslashes on Windows. As a special feature, it is possible to redirect the standard error to the standard output by using `stderr=STDOUT`. Regardless are outputs redirected to files or not, they are accessible through the `result object` returned when the process ends. Examples: | ${result} = | `Run Process` | program | stdout=${TEMPDIR}/stdout.txt | stderr=${TEMPDIR}/stderr.txt | | `Log Many` | stdout: ${result.stdout} | stderr: ${result.stderr} | | ${result} = | `Run Process` | program | stderr=STDOUT | | `Log` | all output: ${result.stdout} | *Note:* The created output files are not automatically removed after the test run. The user is responsible to remove them if needed. == Alias == A custom name given to the process that can be used when selecting the `active process`. Example: | `Start Process` | program | alias=example | = Active process = The test library keeps record which of the started processes is currently active. By default it is latest process started with `Start Process`, but `Switch Process` can be used to select a different one. The keywords that operate on started processes will use the active process by default, but it is possible to explicitly select a different process using the `handle` argument. The handle can be the identifier returned by `Start Process` or an explicitly given `alias`. = Stopping processes = Started processed can be stopped using `Terminate Process` and `Terminate All Processes`. The former is used for stopping a selected process, and the latter to make sure all processes are stopped, for example, in a suite teardown. Both keywords use `subprocess` [http://docs.python.org/2.7/library/subprocess.html#subprocess.Popen.terminate|terminate()] method by default, but can be configured to use [http://docs.python.org/2.7/library/subprocess.html#subprocess.Popen.kill|kill()] instead. Because both `terminate()` and `kill()` methods were added to `subprocess` in Python 2.6, stopping processes does not work with Python or Jython 2.5. Unfortunately at least beta releases of Jython 2.7 [http://bugs.jython.org/issue1898|do not seem to support it either]. Examples: | `Terminate Process` | kill=True | | `Terminate All Processes` | = Result object = `Run Process` and `Wait For Process` keywords return a result object that contains information about the process execution as its attibutes. What is available is documented in the table below. | *Attribute* | *Explanation* | | rc | Return code of the process as an integer. | | stdout | Contents of the standard output stream. | | stderr | Contents of the standard error stream. | | stdout_path | Path where stdout was redirected or `None` if not redirected. | | stderr_path | Path where stderr was redirected or `None` if not redirected. | Example: | ${result} = | `Run Process` | program | | `Should Be Equal As Integers` | ${result.rc} | | | `Should Match` | ${result.stdout} | Some t?xt* | | `Should Be Empty` | ${result.stderr} | | | ${stdout} = | `Get File` | ${result.stdout_path} | | `File Should Be Empty` | ${result.stderr_path} | | | `Should Be Equal` | ${result.stdout} | ${stdout} | = Using with OperatingSystem library = The OperatingSystem library also contains keywords for running processes. They are not as flexible as the keywords provided by this library, and thus not recommended to be used anymore. They may eventually even be deprecated. There is a name collision because both of these libraries have `Start Process` and `Switch Process` keywords. This is handled so that if both libraries are imported, the keywords in the Process library are used by default. If there is a need to use the OperatingSystem variants, it is possible to use `OperatingSystem.Start Process` syntax or use the `BuiltIn` keyword `Set Library Search Order` to change the priority. Other keywords in the OperatingSystem library can be used freely with keywords in the Process library. = Example = | ***** Settings ***** | Library Process | Suite Teardown `Terminate All Processes` kill=True | | ***** Test Cases ***** | Example | `Start Process` program arg1 arg2 alias=First | ${handle} = `Start Process` command.sh arg | command2.sh shell=True cwd=/path | ${result} = `Run Process` ${CURDIR}/script.py | `Should Not Contain` ${result.stdout} FAIL | `Terminate Process` ${handle} | ${result} = `Wait For Process` First | `Should Be Equal As Integers` ${result.rc} 0 """ ROBOT_LIBRARY_SCOPE = 'GLOBAL' ROBOT_LIBRARY_VERSION = get_version() def __init__(self): self._processes = ConnectionCache('No active process.') self._results = {} def run_process(self, command, *arguments, **configuration): """Runs a process and waits for it to complete. See `Specifying command and arguments` and `Process configuration` for more information about the arguments. Returns a `result object` containing information about the execution. This command does not change the `active process`. """ current = self._processes.current try: handle = self.start_process(command, *arguments, **configuration) return self.wait_for_process(handle) finally: self._processes.current = current def start_process(self, command, *arguments, **configuration): """Starts a new process on background. See `Specifying command and arguments` and `Process configuration` for more information about the arguments. Makes the started process new `active process`. Returns an identifier that can be used as a handle to active the started process if needed. """ config = ProcessConfig(**configuration) executable_command = self._cmd(arguments, command, config.shell) logger.info('Starting process:\n%s' % executable_command) logger.debug('Process configuration:\n%s' % config) process = subprocess.Popen(executable_command, stdout=config.stdout_stream, stderr=config.stderr_stream, stdin=subprocess.PIPE, shell=config.shell, cwd=config.cwd, env=config.env, universal_newlines=True) self._results[process] = ExecutionResult(process, config.stdout_stream, config.stderr_stream) return self._processes.register(process, alias=config.alias) def _cmd(self, args, command, use_shell): command = [encode_to_system(item) for item in [command] + list(args)] if not use_shell: return command if args: return subprocess.list2cmdline(command) return command[0] def is_process_running(self, handle=None): """Checks is the process running or not. If `handle` is not given, uses the current `active process`. Returns `True` if the process is still running and `False` otherwise. """ return self._processes[handle].poll() is None def process_should_be_running(self, handle=None, error_message='Process is not running.'): """Verifies that the process is running. If `handle` is not given, uses the current `active process`. Fails if the process has stopped. """ if not self.is_process_running(handle): raise AssertionError(error_message) def process_should_be_stopped(self, handle=None, error_message='Process is running.'): """Verifies that the process is not running. If `handle` is not given, uses the current `active process`. Fails if the process is still running. """ if self.is_process_running(handle): raise AssertionError(error_message) def wait_for_process(self, handle=None): """Waits for the process to complete. If `handle` is not given, uses the current `active process`. Returns a `result object` containing information about the execution. """ process = self._processes[handle] result = self._results[process] logger.info('Waiting for process to complete.') result.rc = process.wait() or 0 logger.info('Process completed.') return result def terminate_process(self, handle=None, kill=False): """Terminates the process. If `handle` is not given, uses the current `active process`. See `Stopping process` for more details. """ process = self._processes[handle] try: terminator = process.kill if kill else process.terminate except AttributeError: raise RuntimeError('Terminating processes is not supported ' 'by this interpreter version.') logger.info('Killing process.' if kill else 'Terminating process.') try: terminator() except OSError: if process.poll() is None: raise logger.debug('Ignored OSError because process was stopped.') def terminate_all_processes(self, kill=True): """Terminates all still running processes started by this library. See `Stopping processes` for more details. """ for handle in range(1, len(self._processes) + 1): if self.is_process_running(handle): self.terminate_process(handle, kill=kill) self.__init__() def get_process_id(self, handle=None): """Returns the process ID (pid) of the process. If `handle` is not given, uses the current `active process`. Returns the pid assigned by the operating system as an integer. Note that with Jython, at least with the 2.5 version, the returned pid seems to always be `None`. The pid is not the same as the identifier returned by `Start Process` that is used internally by this library. """ return self._processes[handle].pid def get_process_object(self, handle=None): """Return the underlying `subprocess.Popen` object. If `handle` is not given, uses the current `active process`. """ return self._processes[handle] def switch_process(self, handle): """Makes the specified process the current `active process`. The handle can be an identifier returned by `Start Process` or the `alias` given to it explicitly. Example: | `Start Process` | prog1 | alias=process1 | | `Start Process` | prog2 | alias=process2 | | # currently active process is process2 | | `Switch Process` | process1 | | # now active process is process 1 | """ self._processes.switch(handle)
class SOAP(object): def __init__(self): self._xml = XML() self._builtin = BuiltIn() self._cache = ConnectionCache() def create_soap_client(self, alias, wsdl=None, endpoint=None): """ Creates SOAP client with specified alias. Arguments: | alias | client alias | | wsdl | path or url to service wsdl | | endpoint | url for service under test | Example usage: | Create Soap Client | client_alias | wsdl=path_to_wsdl${/}ws_example.wsdl | endpoint=http://localhost:8080/ | """ session = Session() session.verify = False if wsdl: wsdl = self._get_wsdl(session, wsdl) client = SOAPClient(session, wsdl, endpoint) self._cache.register(client, alias) def call_soap_method(self, alias, name, message, replace=None, endpoint=None, expect_fault=False, xml=True): """ Call SOAP method. Arguments: | alias | client alias | | name | method name | | message | path to SOAP message or SOAP message | | replace | dictionary of old_value: new_value pairs for replace in message | | endpoint | url for service under test, rewrites client endpoint | | expect_fault | if True, not raise exception when receive fault | | xml | return type, if True - return xml object (XML library format), False - string | Example usage: | ${replace} | Create Dictionary | _id_ | 64 | | ${response} | Call Soap Method | client_alias | getUserName | path_to_soap_message${/}example.xml | replace=${replace} | expect_fault=False | xml=True | | Element Should Exist | ${response} | .//{http://www.example.ru/example}requestResult | """ client = self._cache.switch(alias) if endpoint: client.endpoint = endpoint try: ET.fromstring(message) except ET.ParseError: if not path.isfile(message): raise IOError("File not found or message is not well-formed" % message) message = open(message, mode="r", encoding="utf-8").read() if replace: for k, v in replace.items(): message = message.replace(k, v) status_code, response = self._call(client, name, message) expect_fault = self._builtin.convert_to_boolean(expect_fault) is_fault = self._is_fault(status_code) if is_fault and not expect_fault: raise AssertionError("The server did not raise a fault") elif expect_fault and not is_fault: raise AssertionError("The server not raise a fault") if self._builtin.convert_to_boolean(xml): return self._xml.parse_xml(response, keep_clark_notation=True) else: return response @staticmethod def _get_wsdl(session, url_or_path): if not urlparse(url_or_path).scheme: if not path.isfile(url_or_path): raise IOError("File '%s' not found" % url_or_path) wsdl = open(url_or_path, mode="r", encoding="utf-8").read() else: response = session.get(url_or_path) if response.status_code != 200: raise ConnectionError("Server not found or resource '%s' is not available" % url_or_path) wsdl = response.content.decode("utf-8") return wsdl @staticmethod def _is_fault(status_code): if status_code == 200: return False else: return True def _get_soap_action(self, wsdl, name): root = self._xml.parse_xml(wsdl) xpath = ".//*[@name='%s']/operation" % name return self._xml.get_element_attribute(root, "soapAction", xpath=xpath) def _call(self, client, name, message): if client.wsdl: action = self._get_soap_action(client.wsdl, name) else: action = "" headers = {"Accept-Encoding": "gzip,deflate", "Content-Type": "text/xml;charset=UTF-8", "Connection": "Keep-Alive", "SOAPAction": action} logger.info("Execution '%s' soap method" % name) response = client.session.post(client.endpoint, headers=headers, data=message.encode("utf-8")) return response.status_code, response.content.decode("utf-8")
class PysphereLibrary(object): """Robot Framework test library for VMWare interaction The library has the following main usages: - Identifying available virtual machines on a vCenter or ESXi host - Starting and stopping VMs - Shutting down, rebooting VM guest OS - Checking VM status - Reverting VMs to a snapshot - Retrieving basic VM properties This library is essentially a wrapper around Pysphere http://code.google.com/p/pysphere/ adding connection caching consistent with other Robot Framework libraries. """ ROBOT_LIBRARY_SCOPE = 'GLOBAL' ROBOT_LIBRARY_VERSION = VERSION def __init__(self): """ """ self._connections = ConnectionCache() def open_pysphere_connection(self, host, user, password, alias=None): """Opens a pysphere connection to the given `host` using the supplied `user` and `password`. The new connection is made active and any existing connections are left open in the background. This keyword returns the index of the new connection which can be used later to switch back to it. Indices start from `1` and are reset when `Close All Pysphere Connections` is called. An optional `alias` can be supplied for the connection and used for switching between connections. See `Switch Pysphere Connection` for details. Example: | ${index}= | Open Pysphere Connection | my.vcenter.server.com | username | password | alias=myserver | """ server = VIServer() server.connect(host, user, password) connection_index = self._connections.register(server, alias) logger.info("Pysphere connection opened to host %s" % host) return connection_index def is_connected_to_pysphere(self): return self._connections.current.is_connected() def switch_pysphere_connection(self, index_or_alias): """Switches the active connection by index of alias. `index_or_alias` is either a connection index (an integer) or alias (a string). Index can be obtained by capturing the return value from `Open Pysphere Connection` and alias can be set as a named variable with the same method. Example: | ${my_connection}= | Open Pysphere Connection | myhost | myuser | mypassword | | Open Pysphere Connection | myotherhost | myuser | mypassword | alias=otherhost | | Switch Pysphere Connection | ${my_connection} | | Power On Vm | myvm | | Switch Pysphere Connection | otherhost | | Power On Vm | myothervm | """ old_index = self._connections.current_index if index_or_alias is not None: self._connections.switch(index_or_alias) logger.info("Pysphere connection switched to %s" % index_or_alias) else: logger.info("No index or alias given, pysphere connection has not been switched.") def close_pysphere_connection(self): """Closes the current pysphere connection. No other connection is made active by this keyword. use `Switch Pysphere Connection` to switch to another connection. Example: | ${my_connection}= | Open Pysphere Connection | myhost | myuser | mypassword | | Power On Vm | myvm | | Close Pysphere Connection | """ self._connections.current.disconnect() logger.info("Connection closed, there will no longer be a current pysphere connection.") self._connections.current = self._connections._no_current def close_all_pysphere_connections(self): """Closes all active pysphere connections. This keyword is appropriate for use in test or suite teardown. The assignment of connection indices resets after calling this keyword, and the next connection opened will be allocated index `1`. Example: | ${my_connection}= | Open Pysphere Connection | myhost | myuser | mypassword | | Open Pysphere Connection | myotherhost | myuser | mypassword | alias=otherhost | | Switch Pysphere Connection | ${myserver} | | Power On Vm | myvm | | Switch Pysphere Connection | otherhost | | Power On Vm | myothervm | | [Teardown] | Close All Pysphere Connections | """ self._connections.close_all(closer_method='disconnect') logger.info("All pysphere connections closed.") def get_vm_names(self): """Returns a list of all registered VMs for the currently active connection. """ return self._connections.current.get_registered_vms() def get_vm_properties(self, name): """Returns a dictionary of the properties associated with the named VM. """ vm = self._get_vm(name) return vm.get_properties(from_cache=False) def power_on_vm(self, name): """Power on the vm if it is not already running. This method blocks until the operation is completed. """ if not self.vm_is_powered_on(name): vm = self._get_vm(name) vm.power_on() logger.info("VM %s powered on." % name) else: logger.info("VM %s was already powered on." % name) def power_off_vm(self, name): """Power off the vm if it is not already powered off. This method blocks until the operation is completed. """ if not self.vm_is_powered_off(name): vm = self._get_vm(name) vm.power_off() logger.info("VM %s was powered off." % name) else: logger.info("VM %s was already powered off." % name) def reset_vm(self, name): """Perform a reset on the VM. This method blocks until the operation is completed. """ vm = self._get_vm(name) vm.reset() logger.info("VM %s reset." % name) def shutdown_vm_os(self, name): """Initiate a shutdown in the guest OS in the VM, returning immediately. """ vm = self._get_vm(name) vm.shutdown_guest() logger.info("VM %s shutdown initiated." % name) def reboot_vm_os(self, name): """Initiate a reboot in the guest OS in the VM, returning immediately. """ vm = self._get_vm(name) vm.reboot_guest() logger.info("VM %s reboot initiated." % name) def vm_is_powered_on(self, name): """Returns true if the VM is in the powered on state. """ vm = self._get_vm(name) return vm.is_powered_on() def vm_is_powered_off(self, name): """Returns true if the VM is in the powered off state. """ vm = self._get_vm(name) return vm.is_powered_off() def vm_wait_for_tools(self, name, timeout=15): """Returns true when the VM tools are running. If the tools are not running within the specified timeout, a VIException is raised with the TIME_OUT FaultType. """ vm = self._get_vm(name) return vm.wait_for_tools(timeout) def revert_vm_to_snapshot(self, name, snapshot_name=None): """Revert the named VM to a snapshot. If `snapshot_name` is supplied it is reverted to that snapshot, otherwise it is reverted to the current snapshot. This method blocks until the operation is completed. """ vm = self._get_vm(name) if snapshot_name is None: vm.revert_to_snapshot() logger.info("VM %s reverted to current snapshot." % name) else: vm.revert_to_named_snapshot(snapshot_name) logger.info("VM %s reverted to snapshot %s." % name, snapshot_name) def _get_vm(self, name): connection = self._connections.current if isinstance(name, unicode): name = name.encode("utf8") return connection.get_vm_by_name(name)
class DatabaseLib(HybridCore): """ DatabaseLib is created based on [https://www.sqlalchemy.org/|sqlalchemy]. It support below features: - Database operations(select/insert/update/delete...) - Multi database connections, user could use "Switch Connection" to change current connection - ORM extension support - Extension this libraries easily """ ROBOT_LIBRARY_SCOPE = 'GLOBAL' ROBOT_LIBRARY_VERSION = VERSION def __init__(self, libraryComponents=[]): """ DatabaseLib could be extened through parameter libraryComponents """ self._connections = ConnectionCache() self._sessions = {} super(DatabaseLib, self).__init__(libraryComponents) @property def current(self): return self._connections.current @property def session(self): if self.current not in self._sessions: raise RuntimeError('Session is not created!') return self._sessions[self.current] @keyword def connect_to_db(self, hostOrUrl, port=None, database=None, user=None, password=None, dbPrefix=None, alias=None, **kwargs): """ Connect to database [http://docs.sqlalchemy.org/en/latest/core/engines.html|sqlalchemy] :param hostOrUrl: database hostname or database connection string :param port: database port :param database: database name :param user: database user name :param password: database password :param dbPrefix: format is dialect+driver, dialect is optional :param alias: connection alias, could be used to switch connection :param kwargs: please check [http://docs.sqlalchemy.org/en/latest/core/engines.html|create_engine] to get more details :return: connection index Example: | Connect To Db | 127.0.0.1 | 3306 | test | user | password | mysql | | Connect To Db | mysql://user:[email protected]:3306/test?charset=utf8 | """ if '://' in hostOrUrl: connectStr = hostOrUrl elif 'mysql' in dbPrefix.lower(): connectStr = '%s://%s:%s@%s:%s/%s?charset=utf8' % ( dbPrefix, user, password, hostOrUrl, port, database) else: connectStr = '%s://%s:%s@%s:%s/%s' % (dbPrefix, user, password, hostOrUrl, port, database) logger.debug('Connection String: {0}'.format(connectStr)) engine = create_engine(connectStr, **kwargs) connectionIndex = self._connections.register(engine, alias) return connectionIndex @keyword def switch_connection(self, indexOrAlias): """ Switch database connection :param indexOrAlias: connection alias or index :return: previous index Example: | Connect To Db | 127.0.0.1 | 3306 | test1 | user | password | mysql | connection_1 | | Connect To Db | 127.0.0.1 | 3306 | test2 | user | password | oracle | connection_2 | | Switch Connection | connection_1 | """ oldIndex = self._connections.current_index self._connections.switch(indexOrAlias) return oldIndex @keyword def create_session(self, autoflush=True, autocommit=False, expireOnCommit=True, info=None, **kwargs): """ Create session based on current connection(engine) if session is already for current connection, keyword will return created session directly. This keyword could be used to extend library with ORM :param autoflush: default value is True :param autocommit: default value is False :param expireOnCommit: default value is True :param info: default value is None :param kwargs: Please check Session in sqlalchemy :return: session """ if self.current in self._sessions: return self._sessions[self.current] elif self.current is not None: self.current.echo = 'debug' session = scoped_session( sessionmaker(bind=self.current, autoflush=autoflush, autocommit=autocommit, expire_on_commit=expireOnCommit, info=info, **kwargs)) self._sessions[self.current] = session return session raise RuntimeError( 'Current connection may closed, or not create connection yet!') @keyword def close_connection(self): """ Close current database connection :return: None """ if self.current in self._sessions: self._sessions.pop(self.current) self.current.dispose() self._connections.current = self._connections._no_current @keyword def close_all_connections(self): """ Close all database connections :return: None """ self._sessions.clear() self._connections.close_all('dispose') @keyword def execute(self, sql): """ Execute sql :param sql: sql :return: sqlalchemy ResultProxy """ return self.current.execute(sql) @keyword def query(self, sql, *args, **kwargs): """ Execute query :param sql: sql string :param args: if params in sql want to be replaced by index, use args :param kwargs: if params in sql want to be replaced by key, use kwargs :return: List of ResultProxy Examples: | ${results}= | Query | SELECT {0}, {1} FROM MY_TABLE | c1 | c2 | | ${results}= | Query | SELECT {col1}, {col2} FROM MY_TABLE | col1=c1 | col2=c2 | | ${results}= | Query | SELECT c1, c2 FROM MY_TABLE | | | """ if not args: args = [] if not kwargs: kwargs = {} sql = sql.format(*args, **kwargs) logger.debug('Execute: %s' % sql) resultProxy = self.execute(sql) results = [result for result in resultProxy] logger.debug('Results: %s' % results) return results @keyword def execute_sql_script(self, sqlFile): """ Execute sql script file :param sqlFile: Path to sql script file, not format should be utf-8 :return: None """ with open(sqlFile, 'r', encoding='utf-8') as f: content = f.read() content = content.replace('\ufeff', '') sqlStrList = sqlparse.split( sqlparse.format(content, strip_comments=True)) sqlStrList = [ sqlparse.format(sqlStr.strip(';'), keyword_case='upper', reindent=True) for sqlStr in sqlStrList if sqlStr.strip() ] for sqlStr in sqlStrList: self.execute(sqlStr) @keyword def call_stored_procedure(self, name, *params): """ Call stored procedure :param name: name of stored procedure :param params: parameters of stored procedure :return: results """ connection = self.current.raw_connection() try: cursor = connection.cursor() results = cursor.callproc(name, params) cursor.close() connection.commit() finally: connection.close() return results