Ejemplo n.º 1
0
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
Ejemplo n.º 3
0
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))
Ejemplo n.º 5
0
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
Ejemplo n.º 6
0
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
Ejemplo n.º 7
0
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
Ejemplo n.º 8
0
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)
Ejemplo n.º 9
0
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'))
Ejemplo n.º 10
0
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]
Ejemplo n.º 12
0
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)
Ejemplo n.º 13
0
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
Ejemplo n.º 16
0
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()
Ejemplo n.º 19
0
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)
Ejemplo n.º 20
0
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)
Ejemplo n.º 22
0
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