FIX

Required files:

Certificates howto

To generate the certs needed for the tls example see: .. literalinclude:: ../../../examples/Transports/FIX/certs/readme.txt

test_plan.py

#!/usr/bin/env python
"""
This example demonstrates FIX communication via FixServer and FixClient drivers.

NOTE: The FixServer driver implementation requires select.poll(), which is not
available on all platforms. Typically it is available on POSIX systems but
not on Windows. This example will not run correctly on platforms where
select.poll() is not available.
"""

import sys

from testplan import test_plan
from testplan.report.testing.styles import Style, StyleEnum

import over_one_session
import over_two_sessions


OUTPUT_STYLE = Style(StyleEnum.ASSERTION_DETAIL, StyleEnum.ASSERTION_DETAIL)


# Hard-coding `pdf_path`, 'stdout_style' and 'pdf_style' so that the
# downloadable example gives meaningful and presentable output.
# NOTE: this programmatic arguments passing approach will cause Testplan
# to ignore any command line arguments related to that functionality.
@test_plan(
    name="FIXCommunication",
    pdf_path="report.pdf",
    stdout_style=OUTPUT_STYLE,
    pdf_style=OUTPUT_STYLE,
)
def main(plan):
    """
    Testplan decorated main function to add and execute MultiTests.

    :return: Testplan result object.
    :rtype:  ``testplan.base.TestplanResult``
    """
    plan.add(over_one_session.get_multitest())
    plan.add(over_two_sessions.get_multitest())


if __name__ == "__main__":
    res = main()
    print("Exiting code: {}".format(res.exit_code))
    sys.exit(res.exit_code)

over_one_session.py

"""Tests FIX communication between a server and a client."""

import os
from pathlib import Path
import sys

try:
    sys.path.append(os.environ["PYFIXMSG_PATH"])
    import pyfixmsg
except (KeyError, ImportError):
    raise RuntimeError(
        "Download pyfixmsg library from "
        "https://github.com/morganstanley/pyfixmsg "
        "and set PYFIXMSG_PATH env var to the local path."
    )
try:
    SPEC_FILE = os.environ["FIX_SPEC_FILE"]
except KeyError:
    raise RuntimeError(
        "No spec file set. You should download "
        "https://github.com/quickfix/quickfix/blob/master/spec/FIX42.xml "
        "file and set FIX_SPEC_FILE to the local path."
    )

from pyfixmsg.fixmessage import FixMessage
from pyfixmsg.codecs.stringfix import Codec
from pyfixmsg.reference import FixSpec

from testplan.common.utils.context import context
from testplan.testing.multitest import MultiTest, testsuite, testcase
from testplan.testing.multitest.driver.fix import FixServer, FixClient
from testplan.common.utils.sockets.tls import SimpleTLSConfig

CODEC = Codec(spec=FixSpec(SPEC_FILE))


def fixmsg(source):
    """
    Factory function that forces the codec to our given spec and avoid
    passing codec to serialisation and parsing methods.
    The codec defaults to a reasonable parser but without repeating groups.
    An alternative method is to use the ``to_wire`` and ``from_wire`` methods
    to serialise and parse messages and pass the codec explicitly.
    """
    # python 2 and 3 compatibility
    msg = FixMessage(source)
    msg.codec = CODEC
    return msg


@testsuite
class FIXTestsuite:
    @testcase
    def send_and_receive_msg(self, env, result):
        """
        Basic FIX messaging between a FixServer and a FixClient.
        """
        # First we create a FIX message containing a single tag: 35=D
        msg = fixmsg({35: "D"})

        # We use the client to send that message over to the server.
        # The message is enriched with the expected session tags (49, 56 etc).
        env.client.send(msg)

        # We create a FIX message to describe what we expect the server to
        # receive. We expect the default FIX version FIX.4.2, the same value
        # for tag 35 as given, D, and the correct senderCompID and targetCompID.
        exp_msg = fixmsg(
            {
                8: "FIX.4.2",
                35: "D",
                49: env.client.sender,
                56: env.client.target,
            }
        )

        # We receive the message from the server.
        received = env.server.receive()

        # We assert that we expect a message that matches the message we sent.
        # We restrict the comparison to tags 8, 35, 49 and 56, since we want to
        # ignore the other message-level tags such as 9 and 10 that are
        # automatically added by the connectors.
        result.fix.match(
            exp_msg,
            received,
            description="Message sent by client match.",
            include_tags=[8, 35, 49, 56],
        )

        # Now, we create a response message from the server, confirming receipt
        # of order (message type 8)
        msg = fixmsg({35: "8"})

        # We use the server to send the response to the client.
        env.server.send(msg)

        # we can also create a heartbeat message (message type 0)
        heartbeat = fixmsg({35: "0"})
        # We use the server to send the heartbeat to the client.
        env.server.send(heartbeat)

        # We create a FIX message to describe what we expect the client to
        # receive. The default FIX version FIX.4.2 is expected, together with
        # the right senderCompID and targetCompID.
        exp_msg = fixmsg(
            {
                8: "FIX.4.2",
                35: "8",
                49: env.client.target,
                56: env.client.sender,
            }
        )

        exp_heartbeat = fixmsg(
            {
                8: "FIX.4.2",
                35: "0",
                49: env.client.target,
                56: env.client.sender,
            }
        )

        # We receive the message from the client.
        received = env.client.receive()
        received_heartbeat = env.client.receive()

        # We expect a message that matches the message we sent. We restrict the
        # comparison to tags 8, 35, 49 and 56, since we want to ignore the
        # other message-level tags such as 9 and 10 that are automatically
        # added by the connectors.
        result.fix.match(
            exp_msg,
            received,
            description="Message sent by server match.",
            include_tags=[8, 35, 49, 56],
        )
        result.fix.match(
            exp_heartbeat,
            received_heartbeat,
            description="Message sent by server match.",
            include_tags=[8, 35, 49, 56],
        )


def get_multitest():
    """
    Creates and returns a new MultiTest instance to be added to the plan.
    The environment is a server and a client connecting using the context
    functionality that retrieves host/port of the server after is started.
    """
    test = MultiTest(
        name="OverOneSession",
        suites=[FIXTestsuite()],
        environment=[
            FixServer(name="server", msgclass=FixMessage, codec=CODEC),
            FixClient(
                name="client",
                host=context("server", "{{host}}"),
                port=context("server", "{{port}}"),
                sender="TW",
                target="ISLD",
                msgclass=FixMessage,
                codec=CODEC,
                logoff_at_stop=False,
            ),
        ],
    )
    return test

over_two_sessions.py

"""Tests FIX communication between a server and multiple clients."""

import os
import sys

try:
    sys.path.append(os.environ["PYFIXMSG_PATH"])
    import pyfixmsg
except (KeyError, ImportError):
    raise RuntimeError(
        "Download pyfixmsg library from "
        "https://github.com/morganstanley/pyfixmsg "
        "and set PYFIXMSG_PATH env var to the local path."
    )
try:
    SPEC_FILE = os.environ["FIX_SPEC_FILE"]
except KeyError:
    raise RuntimeError(
        "No spec file set. You should download "
        "https://github.com/quickfix/quickfix/blob/master/spec/FIX42.xml "
        "file and set FIX_SPEC_FILE to the local path."
    )

from pyfixmsg.fixmessage import FixMessage
from pyfixmsg.codecs.stringfix import Codec
from pyfixmsg.reference import FixSpec

from testplan.common.utils.context import context
from testplan.testing.multitest import MultiTest, testsuite, testcase
from testplan.testing.multitest.driver.fix import FixServer, FixClient

CODEC = Codec(spec=FixSpec(SPEC_FILE))


def fixmsg(source):
    """
    Factory function that forces the codec to our given spec and avoid
    passing codec to serialisation and parsing methods.
    The codec defaults to a reasonable parser but without repeating groups.
    An alternative method is to use the ``to_wire`` and ``from_wire`` methods
    to serialise and parse messages and pass the codec explicitly.
    """
    # python 2 and 3 compatibility
    msg = FixMessage(source)
    msg.codec = CODEC
    return msg


@testsuite
class FIXTestsuite:
    @testcase
    def send_and_receive_msgs(self, env, result):
        """
        Basic FIX messaging with many FixClients connecting to one FixServer.
        """
        # First we create a FIX message with tag: 35=D and a comment in tag 58.
        msg1 = fixmsg({35: "D", 58: "first client"})
        # We use the first client to send that message over to the server.
        # The message is enriched with the expected session tags (49, 56 etc).
        env.client1.send(msg1)
        # We create a FIX message to describe what we expect the server to
        # receive. We expect the default FIX version FIX.4.2, the same value
        # for tag 35 as given, D, and the correct senderCompID and targetCompID
        # (those from the first client).
        exp_msg1 = fixmsg(
            {
                8: "FIX.4.2",
                35: "D",
                49: env.client1.sender,
                56: env.client1.target,
                58: "first client",
            }
        )
        # We receive the message from the server. Since the server now has
        # multiple connections, we also need to specify which connection
        # we want to receive the message from. This is indicated through the
        # (senderCompID, targetCompID) pair passed in.
        received1 = env.server.receive(
            (env.client1.target, env.client1.sender)
        )
        # We assert and restrict the comparison to tags 8, 35, 49, 56 and 58,
        # since we want to ignore the other message-level tags such as 9 and 10
        # that are automatically added by the connectors.
        result.fix.match(
            exp_msg1,
            received1,
            description="Message sent by client 1 match.",
            include_tags=[8, 35, 49, 56, 58],
        )

        # We create a very similar message, but with a different comment.
        msg2 = fixmsg({35: "D", 58: "second client"})
        # Now, we send the message from the second client.
        # The message is enriched with the expected session tags (49, 56 etc).
        env.client2.send(msg2)

        # The message we expect is almost identical, except for senderCompID
        # and targetCompID tags, which now identify the second connection.
        exp_msg2 = fixmsg(
            {
                8: "FIX.4.2",
                35: "D",
                49: env.client2.sender,
                56: env.client2.target,
                58: "second client",
            }
        )
        # We receive the message and this time we want to receive from the
        # second client. So, we specify to the server that it should receive
        # from the second connection.
        received2 = env.server.receive(
            (env.client2.target, env.client2.sender)
        )
        # We assert and restrict the comparison to tags 8, 35, 49, 56 and 58,
        # since we want to ignore the other message-level tags such as 9 and 10
        # that are automatically added by the connectors.
        result.fix.match(
            exp_msg2,
            received2,
            description="Message sent by client 2 match.",
            include_tags=[8, 35, 49, 56, 58],
        )

        # Now, we create a response message from the server,
        # confirming receipt of order (message type 8).
        msg = fixmsg({35: "8"})
        # We use the server to send the response to both clients in turn.
        env.server.send(msg, (env.client1.target, env.client1.sender))
        env.server.send(msg, (env.client2.target, env.client2.sender))
        # We create a FIX message to describe what we expect the clients to
        # receive. The default FIX version FIX.4.2 is expected in both messages.
        # However, the senderCompID and targetCompID differ for the two clients.
        exp_msg1 = fixmsg(
            {
                8: "FIX.4.2",
                35: "8",
                49: env.client1.target,
                56: env.client1.sender,
            }
        )
        exp_msg2 = fixmsg(
            {
                8: "FIX.4.2",
                35: "8",
                49: env.client2.target,
                56: env.client2.sender,
            }
        )
        # We receive the message from the clients.
        received1 = env.client1.receive()
        received2 = env.client2.receive()
        # We expect the messages matche the message we sent. We restrict the
        # comparison to tags 8, 35, 49 and 56, since we want to ignore the
        # other message-level tags such as 9 and 10 that are automatically
        # added by the connectors.
        result.fix.match(
            exp_msg1,
            received1,
            description="Msg sent by server to client 1 match.",
            include_tags=[8, 35, 49, 56],
        )
        result.fix.match(
            exp_msg2,
            received2,
            description="Msg sent by server to client 2 match.",
            include_tags=[8, 35, 49, 56],
        )


def get_multitest():
    """
    Creates and returns a new MultiTest instance to be added to the plan.
    The environment is a server and two clients connecting using the context
    functionality that retrieves host/port of the server after is started.

       ------------- client1
       |
    server
       |
       ------------- client2

    """
    test = MultiTest(
        name="OverTwoSessions",
        suites=[FIXTestsuite()],
        environment=[
            FixServer(name="server", msgclass=FixMessage, codec=CODEC),
            FixClient(
                name="client1",
                host=context("server", "{{host}}"),
                port=context("server", "{{port}}"),
                sender="TW",
                target="ISLD",
                msgclass=FixMessage,
                codec=CODEC,
            ),
            FixClient(
                name="client2",
                host=context("server", "{{host}}"),
                port=context("server", "{{port}}"),
                sender="TW2",
                target="ISLD",
                msgclass=FixMessage,
                codec=CODEC,
            ),
        ],
    )
    return test

PDF report

Sample detailed PDF report.

../_images/fix_sessions_example.png