Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/elixir.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ jobs:
otp-version: '25'
install-hex: true
install-rebar: true
- name: Install anybadge
run: pip install anybadge
- name: Install Dependencies
run: mix deps.get
- name: Run Tests
run: mix test
- name: Run Tests and generate coverage badge
run: ./scripts/generate_coverage_badge.sh
- uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage.svg
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ erl_crash.dump

/bench/snapshots/
/bench/graphs/
/coverage.svg
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ExFIX

[![Build Status](https://github.com/santif/ex_fix/actions/workflows/elixir.yml/badge.svg?branch=master)](https://github.com/santif/ex_fix/actions/workflows/elixir.yml)
[![Coverage Status](https://coveralls.io/repos/github/santif/ex_fix/badge.svg?branch=master)](https://coveralls.io/github/santif/ex_fix?branch=master)
[![Coverage](./coverage.svg)](https://github.com/santif/ex_fix/actions/workflows/elixir.yml)
[![Tokei](https://tokei.rs/b1/github/santif/ex_fix?category=code)](https://tokei.rs/b1/github/santif/ex_fix?category=code)
[![Hex.pm Version](http://img.shields.io/hexpm/v/ex_fix.svg?style=flat)](https://hex.pm/packages/ex_fix)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
Expand All @@ -15,7 +15,7 @@ Add `ex_fix` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[{:ex_fix, "~> 0.2.8"}]
[{:ex_fix, "~> 0.2.9"}]
end
```

Expand Down
11 changes: 2 additions & 9 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,13 @@ defmodule ExFix.Mixfile do
def project do
[
app: :ex_fix,
version: "0.2.8",
version: "0.2.9",
elixir: "~> 1.18",
build_embedded: Mix.env() == :prod,
start_permanent: Mix.env() == :prod,
description: description(),
package: package(),
test_coverage: [tool: ExCoveralls],
preferred_cli_env: [
coveralls: :test,
"coveralls.detail": :test,
"coveralls.post": :test,
"coveralls.html": :test
],
test_coverage: [summary: [threshold: 0]],
deps: deps()
]
end
Expand Down Expand Up @@ -44,7 +38,6 @@ defmodule ExFix.Mixfile do
[
{:benchfella, "~> 0.3.5", only: :dev},
{:ex_doc, "~> 0.30", only: :dev, runtime: false},
{:excoveralls, "~> 0.18", only: :test},
{:dialyxir, "~> 1.3", only: [:dev], runtime: false},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
]
Expand Down
10 changes: 0 additions & 10 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
%{
"benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm", "23f27cbc482cbac03fc8926441eb60a5e111759c17642bac005c3225f5eb809d"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
"credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},
"dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"},
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"},
"excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"},
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
"hackney": {:hex, :hackney, "1.24.1", "f5205a125bba6ed4587f9db3cc7c729d11316fa8f215d3e57ed1c067a9703fa9", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "f4a7392a0b53d8bbc3eb855bdcc919cd677358e65b2afd3840b5b3690c4c8a39"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"tzdata": {:hex, :tzdata, "1.1.3", "b1cef7bb6de1de90d4ddc25d33892b32830f907e7fc2fccd1e7e22778ab7dfbc", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d4ca85575a064d29d4e94253ee95912edfb165938743dbf002acdf0dcecb0c28"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
}
12 changes: 12 additions & 0 deletions scripts/generate_coverage_badge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail

mix test --cover | tee coverage_output.log
coverage=$(grep -E "[0-9]+\.[0-9]+% \| Total" coverage_output.log | awk '{print $1}' | tr -d '%')
coverage=${coverage:-0}

anybadge --value="${coverage}" --label=coverage \
--file=coverage.svg \
0=red 80=yellow 90=green >/dev/null

echo "Generated coverage.svg with ${coverage}% coverage"
42 changes: 42 additions & 0 deletions test/default_session_registry_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
defmodule ExFix.DefaultSessionRegistryTest do
use ExUnit.Case

alias ExFix.DefaultSessionRegistry

test "session_on_init replies according to status" do
table = :ex_fix_registry
:ets.delete_all_objects(table)

assert {:error, :notfound} = DefaultSessionRegistry.session_on_init("s1")

DefaultSessionRegistry.session_update_status("s2", :connecting)
assert :ok = DefaultSessionRegistry.session_on_init("s2")

DefaultSessionRegistry.session_update_status("s3", :connected)
assert :wait_to_reconnect = DefaultSessionRegistry.session_on_init("s3")

DefaultSessionRegistry.session_update_status("s4", :disconnecting)
assert {:error, :disconnected} = DefaultSessionRegistry.session_on_init("s4")
end

test "handles process DOWN messages" do
table = :ex_fix_registry
:ets.delete_all_objects(table)

DefaultSessionRegistry.session_update_status("dn1", :connecting)
:ok = DefaultSessionRegistry.session_on_init("dn1")
state = :sys.get_state(DefaultSessionRegistry)
ref1 = Enum.find_value(state.monitor_map, fn {ref, name} -> if name == "dn1", do: ref end)
send(DefaultSessionRegistry, {:DOWN, ref1, :process, self(), :normal})
Process.sleep(10)
assert DefaultSessionRegistry.get_session_status("dn1") == :disconnected

DefaultSessionRegistry.session_update_status("dn2", :connecting)
:ok = DefaultSessionRegistry.session_on_init("dn2")
state = :sys.get_state(DefaultSessionRegistry)
ref2 = Enum.find_value(state.monitor_map, fn {ref, name} -> if name == "dn2", do: ref end)
send(DefaultSessionRegistry, {:DOWN, ref2, :process, self(), :shutdown})
Process.sleep(10)
assert DefaultSessionRegistry.get_session_status("dn2") == :reconnecting
end
end
57 changes: 57 additions & 0 deletions test/ex_fix_api_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
defmodule ExFix.ApiTest do
use ExUnit.Case

alias ExFix.SessionConfig
alias ExFix.TestHelper.FixEmptySessionHandler

defmodule CaptureRegistry do
@behaviour ExFix.SessionRegistry

def get_session_status(_), do: :disconnected

def start_session(name, %SessionConfig{} = config) do
send(self(), {:start_session, name, config})
:ok
end

def stop_session(name) do
send(self(), {:stop_session, name})
:ok
end

def session_on_init(_), do: :ok
def session_update_status(_, _), do: :ok
end

test "start_session_initiator builds config and delegates" do
ExFix.start_session_initiator(
"test_sess",
"S",
"T",
FixEmptySessionHandler,
session_registry: CaptureRegistry,
hostname: "myhost",
port: 12_345,
log_incoming_msg: false
)

assert_receive {:start_session, "test_sess", config}
assert %SessionConfig{name: "test_sess", sender_comp_id: "S", target_comp_id: "T"} = config
assert config.hostname == "myhost"
assert config.port == 12_345
assert config.log_incoming_msg == false

ExFix.stop_session("test_sess", CaptureRegistry)
assert_receive {:stop_session, "test_sess"}
end

test "start_session_initiator uses defaults" do
ExFix.start_session_initiator("defaults", "S", "T", FixEmptySessionHandler, session_registry: CaptureRegistry)

assert_receive {:start_session, "defaults", config}
assert config.hostname == "localhost"
assert config.port == 9876
assert config.log_incoming_msg == true
assert config.transport_mod == :gen_tcp
end
end
7 changes: 5 additions & 2 deletions test/in_message_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ defmodule ExFix.InMessageTest do
%{bin_msg: bin_msg}
end

test "InMessage tests", %{bin_msg: bin_msg} do
test "get_field returns value", %{bin_msg: bin_msg} do
fix_msg = Parser.parse1(bin_msg, Dictionary, 12_345)
assert InMessage.get_field(fix_msg, "49") == "MARKET"
assert InMessage.get_field(fix_msg, "52") == "20161007-16:28:50.802"
end

# assert InMessage.get_field(fix_msg, "SendingTime", TestDict) == Calendar.DateTime.Parse.rfc3339_utc
test "get_field returns nil for missing field", %{bin_msg: bin_msg} do
fix_msg = Parser.parse1(bin_msg, Dictionary, 12_345)
assert InMessage.get_field(fix_msg, "9999") == nil
end
end
17 changes: 17 additions & 0 deletions test/parser_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,23 @@ defmodule ExFix.ParserTest do
assert fix_msg.original_fix_msg == data
end

test "Parse message with invalid begin string" do
data = msg("8=FIX4.2|9=12|10=000|")
fix_msg = Parser.parse1(data, Dictionary, 1)
assert fix_msg.valid == false
assert fix_msg.error_reason == :begin_string_error
assert fix_msg.original_fix_msg == data
end

test "Parse message with unexpected seqnum" do
now = DateTime.from_naive!(~N[2017-07-17 17:50:56], "Etc/UTC")
data = build_message("0", 10, "SENDER", "TARGET", now)
fix_msg = Parser.parse1(data, Dictionary, 5)
assert fix_msg.valid == false
assert fix_msg.error_reason == :unexpected_seqnum
assert fix_msg.seqnum == 10
end

test "Parse message - stage 1 - subject with 2 fields" do
data =
msg(
Expand Down
14 changes: 14 additions & 0 deletions test/session_sup_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
defmodule ExFix.SessionSupTest do
use ExUnit.Case

test "start_link when already started" do
assert {:error, {:already_started, _}} = ExFix.SessionSup.start_link([])
end

test "dynamic supervisor can start child" do
{:ok, pid} = DynamicSupervisor.start_child(ExFix.SessionSup, {Task, fn -> :timer.sleep(10) end})
assert is_pid(pid)
ref = Process.monitor(pid)
assert_receive {:DOWN, ^ref, :process, ^pid, :normal}, 50
end
end
36 changes: 36 additions & 0 deletions test/session_worker_failure_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
defmodule ExFix.SessionWorkerFailureTest do
use ExUnit.Case

alias ExFix.SessionConfig
alias ExFix.TestHelper.FixEmptySessionHandler

defmodule FailTransport do
def connect(_host, _port, _opts), do: {:error, :econnrefused}
def send(_conn, _data), do: :ok
def close(_conn), do: :ok
end

alias ExFix.TestHelper.TestSessionRegistry

test "worker reports reconnecting on connection failure" do
Process.flag(:trap_exit, true)
{:ok, _} = TestSessionRegistry.start_link()
config = %SessionConfig{
name: "fail1",
mode: :initiator,
sender_comp_id: "S",
target_comp_id: "T",
session_handler: FixEmptySessionHandler,
transport_mod: FailTransport,
transport_options: [],
log_incoming_msg: false,
log_outgoing_msg: false,
reconnect_interval: 0
}

{:ok, pid} = ExFix.SessionWorker.start_link(config, TestSessionRegistry)
ref = Process.monitor(pid)
assert_receive {:DOWN, ^ref, :process, ^pid, :econnrefused}
assert TestSessionRegistry.get_session_status("fail1") == :reconnecting
end
end
61 changes: 61 additions & 0 deletions test/session_worker_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,65 @@ defmodule ExFix.SessionWorkerTest do
SessionWorker.stop("sessiontest1")
assert TestSessionRegistry.get_session_status("sessiontest1") == :disconnected
end

test "higher seqnum triggers resend request" do

cfg = %SessionConfig{
name: "resend_sess",
mode: :initiator,
sender_comp_id: "SENDER",
target_comp_id: "TARGET",
session_handler: FixEmptySessionHandler,
dictionary: DefaultDictionary,
transport_mod: TestTransport,
transport_options: [test_pid: self()]
}

{:ok, _pid} = SessionWorker.start_link(cfg, TestSessionRegistry)
assert_receive {:data, _logon_msg}

now = DateTime.utc_now()
logon = %MessageToSend{seqnum: 1, sender: "TARGET", orig_sending_time: now, target: "SENDER", msg_type: "A", body: []}
TestTransport.receive_data("resend_sess", Serializer.serialize(logon, now))
Process.sleep(20)

msg = %MessageToSend{seqnum: 12, sender: "TARGET", orig_sending_time: now, target: "SENDER", msg_type: "8", body: []}
TestTransport.receive_data("resend_sess", Serializer.serialize(msg, now))

assert_receive {:data, resend}
parsed = Parser.parse(resend, DefaultDictionary, 1)
assert parsed.msg_type == "2"
SessionWorker.stop("resend_sess")
end

test "invalid SenderCompID triggers logout" do
cfg = %SessionConfig{
name: "badcomp",
mode: :initiator,
sender_comp_id: "SENDER",
target_comp_id: "TARGET",
session_handler: FixEmptySessionHandler,
dictionary: DefaultDictionary,
transport_mod: TestTransport,
transport_options: [test_pid: self()]
}

{:ok, pid} = SessionWorker.start_link(cfg, TestSessionRegistry)
ref = Process.monitor(pid)
assert_receive {:data, _}

now = DateTime.utc_now()
logon = %MessageToSend{seqnum: 1, sender: "TARGET", orig_sending_time: now, target: "SENDER", msg_type: "A", body: []}
TestTransport.receive_data("badcomp", Serializer.serialize(logon, now))
Process.sleep(20)

msg = %MessageToSend{seqnum: 2, sender: "OTHER", orig_sending_time: now, target: "SENDER", msg_type: "8", body: []}
TestTransport.receive_data("badcomp", Serializer.serialize(msg, now))

assert_receive {:data, reject}
assert_receive {:data, logout}
assert Parser.parse(reject, DefaultDictionary, 1).msg_type == "3"
assert Parser.parse(logout, DefaultDictionary, 1).msg_type == "5"
assert_receive {:DOWN, ^ref, :process, ^pid, :normal}, 1000
end
end