def test_serve_socket_with_no_request_received(self):
        mock_connection_socket = MagicMock()
        mock_address = MagicMock()
        mock_socket = MagicMock()
        mock_socket.accept = MagicMock(return_value=(mock_connection_socket,
                                                     mock_address))
        mock_handler_result = MagicMock()
        mock_request_handler = MagicMock(return_value=mock_handler_result)
        mock_socket_reader = MagicMock(return_value=None)
        mock_socket_writer = MagicMock()

        return_value = serve_socket(mock_socket,
                                    mock_request_handler,
                                    socket_reader=mock_socket_reader,
                                    socket_writer=mock_socket_writer)

        mock_socket.accept.assert_called_once_with()
        mock_socket_reader.assert_called_once_with(mock_connection_socket)
        self.assertEqual(mock_request_handler.call_count, 0)
        self.assertEqual(mock_socket_writer.call_count, 0)
        mock_connection_socket.close.assert_called_once_with()
        self.assertEqual(return_value, None)
    def test_serve_socket_with_timeout(self):
        mock_connection_socket = MagicMock()
        mock_address = MagicMock()
        mock_socket = MagicMock()
        mock_socket.accept = MagicMock(side_effect=timeout)
        mock_handler_result = MagicMock()
        mock_request_handler = MagicMock(return_value=mock_handler_result)
        mock_request = MagicMock()
        mock_socket_reader = MagicMock(return_value=mock_request)
        mock_socket_writer = MagicMock()

        return_value = serve_socket(mock_socket,
                                    mock_request_handler,
                                    socket_reader=mock_socket_reader,
                                    socket_writer=mock_socket_writer)

        mock_socket.accept.assert_called_once_with()
        self.assertEqual(mock_socket_reader.call_count, 0)
        self.assertEqual(mock_request_handler.call_count, 0)
        self.assertEqual(mock_socket_writer.call_count, 0)
        self.assertEqual(mock_connection_socket.call_count, 0)
        self.assertEqual(mock_connection_socket.close.call_count, 0)
        self.assertEqual(return_value, None)
    def test_serve_socket(self):
        mock_connection_socket = MagicMock()
        mock_address = MagicMock()
        mock_socket = MagicMock()
        mock_socket.accept = MagicMock(return_value=(mock_connection_socket,
                                                     mock_address))
        mock_handler_result = MagicMock()
        mock_request_handler = MagicMock(return_value=mock_handler_result)
        mock_request = MagicMock()
        mock_socket_reader = MagicMock(return_value=mock_request)
        mock_socket_writer = MagicMock()

        return_value = serve_socket(mock_socket,
                                    mock_request_handler,
                                    socket_reader=mock_socket_reader,
                                    socket_writer=mock_socket_writer)

        mock_socket.accept.assert_called_once_with()
        mock_socket_reader.assert_called_once_with(mock_connection_socket)
        mock_request_handler.assert_called_once_with(mock_request)
        mock_socket_writer.assert_called_once_with(
            mock_connection_socket, mock_handler_result.response)
        mock_connection_socket.close.assert_called_once_with()
        self.assertEqual(return_value, mock_handler_result.return_value)
Exemple #4
0
def trail_manager(root_step,
                  api_socket,
                  backup,
                  delay=5,
                  context=None,
                  done_states=DONE_STATES,
                  ignore_states=IGNORE_STATES,
                  state_transitions=STATE_TRANSITIONS):
    """Manage a trail.

    This is the lowest layer of execution of a trail, a Layer 1 client that directly manages a trail.

    Using this function directly requires no knowledge of the working of autotrail, but the requirements for using this
    client should be fulfilled. They are detailed in the documentation below.

    Arguments:
    root_step         -- A Step like object fulfilling the same contract of states it can be in. A trail is represented
                         by the a DAG starting at root_step. A DAG can be created from a list of ordered pairs of Step
                         objects using the make_dag function provided in this module.
    api_socket        -- A socket.socket object where the API is served. All API calls are recieved and responded via
                         this socket.
    backup            -- A call-back function to backup the state of the steps. This function should accept only one
                         parameter viz., the root_step. It will be called with every iteration of the main trail loop
                         to store the state of the DAG. Avoid making this a high latency function to keep the trail
                         responsive.
                         The return value of this function is ignored.
                         Ensure this function is exception safe as an exception here would break out of the trail
                         manager loop.

    Keyword arguments:
    delay             -- The delay before each iteration of the loop, this is the delay with which the trail_manager
                         iterates over the steps it is keeping track of.
                         It is also the delay with which it checks for any API calls.
                         Having a long delay will make the trail less responsive to API calls.
    context           -- Any object that needs to be passed to the action functions as an argument when they are run.
    done_states       -- A list of step states that are considered to be "done". If a step is found in this state, it
                         can be considered to have been run.
    ignore_states     -- A list of step states that will be ignored. A step in these states cannot be traversed over,
                         i.e., all downstream steps will be out of traversal and will never be reached (case when a
                         step has failed etc).
    state_transitions -- A mapping of step states to functions that will be called if a step is found in that state.
                         These functions will be called with 2 parameters - the step and context. Their return value is
                         ignored. These functions can produce side effects by altering the state of the step if needed.

    Responsibilities of the manager include:
    1) Run the API call server.
    2) Iterate over the steps in a topological traversal based on the state_functions and ignorable_state_functions
       data structures.
    3) Invoked the call-back backup function to save the trail state.
    4) Return when the API server shuts down.
    """
    logging.debug('Starting trail manager.')

    # Preparing a list of steps as these are frequently needed for serving the API calls, but topological traversal
    # is expensive whereas, iteration over a list is not.
    steps = list(topological_traverse(root_step))

    # The trail_manager uses the topological_while function, which provides a way to traverse vertices in a topological
    # order (guaranteeing the trail order) while allowing the trail_manager to control the flow.
    # The topological traversal has the effect that a step is not acted upon, unless all its parents are done.
    # The done_check and ignore_check call-backs allow us to tell the topological_while function if a step can be
    # considered done or not.
    done_check = lambda step: True if step.state in done_states else False
    ignore_check = lambda step: True if step.state in ignore_states else False
    step_iterator = topological_while(root_step, done_check, ignore_check)

    while True:
        # It is important to first serve API calls before working on steps because we want a user's request to
        # affect any change before the trail run changes it.
        to_continue = serve_socket(api_socket,
                                   partial(handle_api_call, steps=steps))
        if to_continue is False:
            logging.info('API Server says NOT to continue. Shutting down.')
            api_socket.shutdown(SHUT_RDWR)
            break

        step = next(step_iterator, None)
        if step and step.state in state_transitions:
            state_transitions[step.state](step, context)

        backup(root_step)
        sleep(delay)