From 6e5ea42cb840c4a83188a9cda17d8f4fda988ff4 Mon Sep 17 00:00:00 2001 From: Grant Birchmeier Date: Wed, 4 Jan 2023 18:10:39 -0600 Subject: [PATCH 1/4] port @mgatny's "JSON to FIX" work to head porting @mgatny's #745 --- Examples/JsonToFix/Examples.JsonToFix.csproj | 16 ++++ Examples/JsonToFix/Program.cs | 77 +++++++++++++++++ QuickFIXn.sln | 10 +++ QuickFIXn/Message/Message.cs | 67 +++++++++++++++ UnitTests/MessageTests.cs | 89 ++++++++++++++++++++ 5 files changed, 259 insertions(+) create mode 100644 Examples/JsonToFix/Examples.JsonToFix.csproj create mode 100644 Examples/JsonToFix/Program.cs diff --git a/Examples/JsonToFix/Examples.JsonToFix.csproj b/Examples/JsonToFix/Examples.JsonToFix.csproj new file mode 100644 index 000000000..22756adfd --- /dev/null +++ b/Examples/JsonToFix/Examples.JsonToFix.csproj @@ -0,0 +1,16 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + diff --git a/Examples/JsonToFix/Program.cs b/Examples/JsonToFix/Program.cs new file mode 100644 index 000000000..e33c3bee8 --- /dev/null +++ b/Examples/JsonToFix/Program.cs @@ -0,0 +1,77 @@ +using System; +using System.Text; +using System.Text.Json; +using System.IO; +using QuickFix; + +namespace TradeClient +{ + class Program + { + static void JsonMsgToFix(string json, QuickFix.DataDictionary.DataDictionary sessionDataDictionary, QuickFix.DataDictionary.DataDictionary appDataDictionary, QuickFix.IMessageFactory msgFactory) + { + var msg = new Message(); + msg.FromJson(json, true, sessionDataDictionary, appDataDictionary, msgFactory); + Console.WriteLine(msg.ToString()); + } + + static void JsonToFix(string fname, QuickFix.DataDictionary.DataDictionary sessionDataDictionary, QuickFix.DataDictionary.DataDictionary appDataDictionary) + { + try + { + QuickFix.IMessageFactory msgFactory = new QuickFix.DefaultMessageFactory(); + string json = File.ReadAllText(fname); + using (JsonDocument document = JsonDocument.Parse(json)) + { + if (document.RootElement.TryGetProperty("messages", out JsonElement messagesElement)) + { + foreach (JsonElement jsonMsg in messagesElement.EnumerateArray()) + { + JsonMsgToFix(jsonMsg.ToString(), sessionDataDictionary, appDataDictionary, msgFactory); + } + } + else // assume there is only one message instead of an array of messages + { + JsonMsgToFix(json, sessionDataDictionary, appDataDictionary, msgFactory); + } + } + } + catch (System.Exception e) + { + Console.WriteLine(e.Message); + Console.WriteLine(e.StackTrace); + } + } + + [STAThread] + static void Main(string[] args) + { + if (args.Length < 1 || args.Length > 2) + { + System.Console.WriteLine("USAGE"); + System.Console.WriteLine(""); + System.Console.WriteLine(" FixToJson.exe FILE DATA_DICTIONARY"); + System.Console.WriteLine(""); + System.Console.WriteLine(" The FILE may contain either a single message in FIX JSON Encoding, or an array of messages in a root-level \"messages\" element."); + System.Console.WriteLine(""); + System.Console.WriteLine("EXAMPLES"); + System.Console.WriteLine(""); + System.Console.WriteLine(" JsonToFix.exe messages.json ../../spec/fix/FIX50SP2.xml"); + System.Console.WriteLine(" JsonToFix.exe messages.json ../../spec/fix/FIX44.xml"); + System.Console.WriteLine(" JsonToFix.exe messages.json ../../spec/fix/FIX42.xml"); + System.Console.WriteLine(""); + System.Console.WriteLine("NOTE"); + System.Console.WriteLine(""); + System.Console.WriteLine(" Per the FIX JSON Encoding Specification, tags are converted to human-readable form, but values are not."); + System.Environment.Exit(2); + } + + string fname = args[0]; + QuickFix.DataDictionary.DataDictionary sessionDataDictionary = new QuickFix.DataDictionary.DataDictionary(args[1]); + QuickFix.DataDictionary.DataDictionary appDataDictionary = sessionDataDictionary; + + JsonToFix(fname, sessionDataDictionary, appDataDictionary); + Environment.Exit(1); + } + } +} diff --git a/QuickFIXn.sln b/QuickFIXn.sln index 99eb3b78c..af62f1e7b 100644 --- a/QuickFIXn.sln +++ b/QuickFIXn.sln @@ -50,6 +50,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuickFix.FIX50SP2", "Messag EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuickFix.FIXT11", "Messages\FIXT11\QuickFix.FIXT11.csproj", "{6EABF160-E21A-4ABD-82C9-0DCC085BDE07}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples.JsonToFix", "Examples\JsonToFix\Examples.JsonToFix.csproj", "{68D01488-2B63-450C-A0D0-C6426C9E4AE7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -178,6 +180,14 @@ Global {6EABF160-E21A-4ABD-82C9-0DCC085BDE07}.Release|Any CPU.Build.0 = Release|Any CPU {6EABF160-E21A-4ABD-82C9-0DCC085BDE07}.Release|x64.ActiveCfg = Release|x64 {6EABF160-E21A-4ABD-82C9-0DCC085BDE07}.Release|x64.Build.0 = Release|x64 + {68D01488-2B63-450C-A0D0-C6426C9E4AE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68D01488-2B63-450C-A0D0-C6426C9E4AE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68D01488-2B63-450C-A0D0-C6426C9E4AE7}.Debug|x64.ActiveCfg = Debug|Any CPU + {68D01488-2B63-450C-A0D0-C6426C9E4AE7}.Debug|x64.Build.0 = Debug|Any CPU + {68D01488-2B63-450C-A0D0-C6426C9E4AE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68D01488-2B63-450C-A0D0-C6426C9E4AE7}.Release|Any CPU.Build.0 = Release|Any CPU + {68D01488-2B63-450C-A0D0-C6426C9E4AE7}.Release|x64.ActiveCfg = Release|Any CPU + {68D01488-2B63-450C-A0D0-C6426C9E4AE7}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/QuickFIXn/Message/Message.cs b/QuickFIXn/Message/Message.cs index c3f7bd402..47b18527c 100644 --- a/QuickFIXn/Message/Message.cs +++ b/QuickFIXn/Message/Message.cs @@ -2,6 +2,7 @@ using System.Text; using QuickFix.Fields; using System.Text.RegularExpressions; +using System.Text.Json; using System.Collections.Generic; namespace QuickFix @@ -489,6 +490,72 @@ public void FromString(string msgstr, bool validate, } } + /// + /// Creates a Message from FIX JSON Encoding. + /// See: https://github.com/FIXTradingCommunity/fix-json-encoding-spec + /// + /// + /// + /// + /// + /// If null, any groups will be constructed as generic Group objects + public void FromJson(string json, bool validate, DataDictionary.DataDictionary sessionDD, DataDictionary.DataDictionary appDD, IMessageFactory msgFactory) + { + this.ApplicationDataDictionary = appDD; + Clear(); + + using (JsonDocument document = JsonDocument.Parse(json)) + { + string beginString = document.RootElement.GetProperty("Header").GetProperty("BeginString").GetString(); + string msgType = document.RootElement.GetProperty("Header").GetProperty("MsgType").GetString(); + DataDictionary.IFieldMapSpec msgMap = appDD.GetMapForMessage(msgType); + FromJson(document.RootElement.GetProperty("Header"), beginString, msgType, msgMap, msgFactory, sessionDD, this.Header); + FromJson(document.RootElement.GetProperty("Body"), beginString, msgType, msgMap, msgFactory, appDD, this); + FromJson(document.RootElement.GetProperty("Trailer"), beginString, msgType, msgMap, msgFactory, sessionDD, this.Trailer); + } + + this.Header.SetField(new BodyLength(BodyLength()), true); + this.Trailer.SetField(new CheckSum(Fields.Converters.CheckSumConverter.Convert(CheckSum())), true); + + if (validate) + { + Validate(); + } + } + + protected void FromJson(JsonElement jsonElement, string beginString, string msgType, DataDictionary.IFieldMapSpec msgMap, IMessageFactory msgFactory, DataDictionary.DataDictionary dataDict, FieldMap fieldMap) + { + foreach (JsonProperty field in jsonElement.EnumerateObject()) + { + DataDictionary.DDField ddField; + if (dataDict.FieldsByName.TryGetValue(field.Name.ToString(), out ddField)) + { + if ((null != msgMap) && (msgMap.IsGroup(ddField.Tag)) && (JsonValueKind.Array == field.Value.ValueKind)) + { + foreach (JsonElement jsonGrp in field.Value.EnumerateArray()) + { + Group grp = msgFactory.Create(beginString, msgType, ddField.Tag); + FromJson(jsonGrp, beginString, msgType, msgMap.GetGroupSpec(ddField.Tag), msgFactory, dataDict, grp); + fieldMap.AddGroup(grp); + } + } + + if (JsonValueKind.Array != field.Value.ValueKind) + { + fieldMap.SetField(new StringField(ddField.Tag, field.Value.ToString())); + } + } + else + { + // this may be a custom tag given by number instead of name + if (Int32.TryParse(field.Name.ToString(), out int customTagNumber)) + { + fieldMap.SetField(new StringField(customTagNumber, field.Value.ToString())); + } + } + } + } + /// /// Constructs a group and stores it in this Message object /// diff --git a/UnitTests/MessageTests.cs b/UnitTests/MessageTests.cs index e135f1a1b..b7d4c4ae0 100644 --- a/UnitTests/MessageTests.cs +++ b/UnitTests/MessageTests.cs @@ -963,6 +963,95 @@ public void ChecksumIsLastFieldOfTrailer() string foo = msg.ToString().Replace(Message.SOH, "|"); StringAssert.EndsWith("|10=099|", foo); + } + + [Test] + [Category("JSON")] + public void JsonNestedRepeatingGroupParseGroupTest() + { + // Given the following string in FIX JSON Encoding: + string json = @" + { + ""Header"": { + ""BeginString"":""FIX.4.4"", + ""MsgSeqNum"":""360"", + ""MsgType"":""8"", + ""SenderCompID"":""BLPTSOX"", + ""SendingTime"":""20130321-15:21:23"", + ""TargetCompID"":""THINKTSOX"" + }, + ""Body"": { + ""31337"":""custom body field"", + ""AvgPx"":""122.255"", + ""ClOrdID"":""61101189"", + ""CumQty"":""1990000"", + ""ExecID"":""VCON:20130321:50018:5:12"", + ""LastPx"":""122.255"", + ""LastQty"":""1990000"", + ""OrderID"":""116"", + ""OrderQty"":""1990000"", + ""OrdStatus"":""2"", + ""Side"":""1"", + ""Symbol"":""[N/A]"", + ""TransactTime"":""20130321-15:21:23"", + ""ExecType"":""F"", + ""LeavesQty"":""0"", + ""NoPartyIDs"": [ + { + ""PartyIDSource"":""D"", + ""PartyID"":""OHAI"", + ""PartyRole"":""1"", + ""NoPartySubIDs"": [ + { + ""PartySubID"":""14"", + ""PartySubIDType"":""4"", + ""31338"":""custom group field"" + } + ] + }, + { ""PartyIDSource"":""D"", ""PartyID"":""TFOLIO:6804469"", ""PartyRole"":""12"" }, + { ""PartyIDSource"":""D"", ""PartyID"":""TFOLIO"", ""PartyRole"":""11"" }, + { ""PartyIDSource"":""D"", ""PartyID"":""THINKFOLIO LTD"", ""PartyRole"":""13"" }, + { ""PartyIDSource"":""D"", ""PartyID"":""SXT"", ""PartyRole"":""16"" }, + { ""PartyIDSource"":""D"", ""PartyID"":""TFOLIO:6804469"", ""PartyRole"":""36"" } + ] + }, + ""Trailer"": { + } + } + "; + + // When the JSON is parsed into a QuickFIX Message + var dd = new QuickFix.DataDictionary.DataDictionary(); + dd.LoadFIXSpec("FIX44"); + var msg = new Message(); + msg.FromJson(json, true, dd, dd, _defaultMsgFactory); + TestContext.Out.WriteLine(msg.ToString().Replace(Message.SOH, "|")); + + // Then the Header of the Message should contain: + Assert.That(msg.Header.GetString(Tags.BeginString), Is.EqualTo("FIX.4.4")); + Assert.That(msg.Header.GetString(Tags.MsgSeqNum), Is.EqualTo("360")); + Assert.That(msg.Header.GetString(Tags.BodyLength), Is.EqualTo("446")); + + // And the Body of the Message should contain: + Assert.That(msg.GetString(31337), Is.EqualTo("custom body field")); + Assert.That(msg.GetString(Tags.AvgPx), Is.EqualTo("122.255")); + Assert.That(msg.GetString(Tags.Symbol), Is.EqualTo("[N/A]")); + Assert.That(msg.GetString(Tags.OrdStatus), Is.EqualTo("2")); + Assert.That(msg.GetString(Tags.TransactTime), Is.EqualTo("20130321-15:21:23")); + + // And the NoPartyIDs Group should contain: + Assert.That(msg.GetString(Tags.NoPartyIDs), Is.EqualTo("6")); + + var noPartyGrp = msg.GetGroup(1, Tags.NoPartyIDs); + Assert.That(noPartyGrp.GetString(Tags.PartyID), Is.EqualTo("OHAI")); + Assert.That(noPartyGrp.GetString(Tags.PartyIDSource), Is.EqualTo("D")); + Assert.That(noPartyGrp.GetString(Tags.NoPartySubIDs), Is.EqualTo("1")); + + var noPartySubGrp = noPartyGrp.GetGroup(1, Tags.NoPartySubIDs); + Assert.That(noPartySubGrp.GetString(Tags.PartySubID), Is.EqualTo("14")); + Assert.That(noPartySubGrp.GetString(Tags.PartySubIDType), Is.EqualTo("4")); + Assert.That(noPartySubGrp.GetString(31338), Is.EqualTo("custom group field")); } } } From 059ee2fe3005c0eb924d709a27bc6d46244f356a Mon Sep 17 00:00:00 2001 From: Grant Birchmeier Date: Wed, 4 Jan 2023 18:19:13 -0600 Subject: [PATCH 2/4] release notes for JSON<->FIX feature-adds --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4c0a2441c..5db9d7be9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -21,6 +21,8 @@ it technically violates semantic versioning. change various Get/SetNextSenderMsgSeqNum & Get/SetNextTargetMsgSeqNum functions to properties (gbirchmeier) **Non-breaking changes** +* (minor) #745 - JSON-to-FIX (mgatny) +* (minor) #724 - FIX-to-JSON serialization, and a ToXML() bugfix (mgatny) * (patch) #647 - replace lock with memory barrier to avoid deadlocks (brunobelmondo) * (patch) #623 - fix issues with New-Release.ps1 (fourpastmidnight) * (minor) #732 - generate FIXT11 msg classes so they can be cracked (mgatny) From c8d46d82104cfb89d2a509ecb9869dfa1d864516 Mon Sep 17 00:00:00 2001 From: Oliver Clancy Date: Fri, 14 Jul 2023 16:23:34 +0100 Subject: [PATCH 3/4] Reverse logic for testing TooLow and TooHigh when recieving a SequenceReset with GapFill=Y Reversing logic allows the GapFill to be applied during message resend. Discussion: https://github.com/connamara/quickfixn/issues/309#issuecomment-1635928629 --- QuickFIXn/Session.cs | 16 +++++++------- UnitTests/SessionTest.cs | 46 ++++++++++++++++++++-------------------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/QuickFIXn/Session.cs b/QuickFIXn/Session.cs index 128dae377..6ac8a2061 100755 --- a/QuickFIXn/Session.cs +++ b/QuickFIXn/Session.cs @@ -463,7 +463,7 @@ public void Next() if (!IsSessionTime) { - if(IsInitiator) + if (IsInitiator) Reset("Out of SessionTime (Session.Next())"); else Reset("Out of SessionTime (Session.Next())", "Message received outside of session time"); @@ -830,7 +830,7 @@ protected void NextResendRequest(Message resendReq) { initializeResendFields(msg); - if(!ResendApproved(msg, SessionID)) + if (!ResendApproved(msg, SessionID)) { continue; } @@ -926,7 +926,7 @@ protected void NextSequenceReset(Message sequenceReset) if (sequenceReset.IsSetField(Fields.Tags.GapFillFlag)) isGapFill = sequenceReset.GetBoolean(Fields.Tags.GapFillFlag); - if (!Verify(sequenceReset, isGapFill, isGapFill)) + if (!Verify(sequenceReset, !isGapFill, !isGapFill)) return; if (sequenceReset.IsSetField(Fields.Tags.NewSeqNo)) @@ -1058,7 +1058,7 @@ public void Reset(string loggedReason) /// message to put in the Logout message's Text field (ignored if null/empty string) public void Reset(string loggedReason, string logoutMessage) { - if(this.IsLoggedOn) + if (this.IsLoggedOn) GenerateLogout(logoutMessage); Disconnect("Resetting..."); state_.Reset(loggedReason); @@ -1150,7 +1150,7 @@ protected void DoPossDup(Message msg) { // If config RequiresOrigSendingTime=N, then tolerate SequenceReset messages that lack OrigSendingTime (issue #102). // (This field doesn't really make sense in this message, so some parties omit it, even though spec requires it.) - string msgType = msg.Header.GetString(Fields.Tags.MsgType); + string msgType = msg.Header.GetString(Fields.Tags.MsgType); if (msgType == Fields.MsgType.SEQUENCE_RESET && RequiresOrigSendingTime == false) return; @@ -1388,7 +1388,7 @@ internal bool GenerateReject(MessageBuilder msgBuilder, FixValues.SessionRejectR { return GenerateReject(msgBuilder.RejectableMessage(), reason, 0); } - + internal bool GenerateReject(MessageBuilder msgBuilder, FixValues.SessionRejectReason reason, int field) { return GenerateReject(msgBuilder.RejectableMessage(), reason, field); @@ -1533,7 +1533,7 @@ protected void InsertSendingTime(FieldMap header) else fix42OrAbove = this.SessionID.BeginString.CompareTo(FixValues.BeginString.FIX42) >= 0; - header.SetField(new Fields.SendingTime(System.DateTime.UtcNow, fix42OrAbove ? TimeStampPrecision : TimeStampPrecision.Second ) ); + header.SetField(new Fields.SendingTime(System.DateTime.UtcNow, fix42OrAbove ? TimeStampPrecision : TimeStampPrecision.Second)); } protected void Persist(Message message, string messageString) @@ -1595,7 +1595,7 @@ protected void InsertOrigSendingTime(FieldMap header, System.DateTime sendingTim else fix42OrAbove = this.SessionID.BeginString.CompareTo(FixValues.BeginString.FIX42) >= 0; - header.SetField(new OrigSendingTime(sendingTime, fix42OrAbove ? TimeStampPrecision : TimeStampPrecision.Second ) ); + header.SetField(new OrigSendingTime(sendingTime, fix42OrAbove ? TimeStampPrecision : TimeStampPrecision.Second)); } protected void NextQueued() { diff --git a/UnitTests/SessionTest.cs b/UnitTests/SessionTest.cs index ccfa316f4..b9ff7d3f4 100755 --- a/UnitTests/SessionTest.cs +++ b/UnitTests/SessionTest.cs @@ -12,7 +12,7 @@ class MockResponder : QuickFix.IResponder #region Responder Members QuickFix.DefaultMessageFactory messageFactory = new QuickFix.DefaultMessageFactory(); - + public Dictionary> msgLookup = new Dictionary>(); public Queue dups = new Queue(); @@ -38,7 +38,7 @@ public bool Send(string msgStr) if (message.Header.IsSetField(possDup)) message.Header.GetField(possDup); - if (possDup.getValue() && msgType.getValue()!= QuickFix.Fields.MsgType.SEQUENCE_RESET) + if (possDup.getValue() && msgType.getValue() != QuickFix.Fields.MsgType.SEQUENCE_RESET) { dups.Enqueue(message); } @@ -97,7 +97,7 @@ public void ToApp(QuickFix.Message message, QuickFix.SessionID sessionId) { if (doNotSendException != null) throw doNotSendException; - + } public void FromApp(QuickFix.Message message, QuickFix.SessionID sessionID) @@ -198,7 +198,7 @@ public void Setup() // acceptor session = new QuickFix.Session(false, application, new QuickFix.MemoryStoreFactory(), sessionID, - new QuickFix.DataDictionaryProvider(),new QuickFix.SessionSchedule(config), 0, logFactory, new QuickFix.DefaultMessageFactory(), "blah"); + new QuickFix.DataDictionaryProvider(), new QuickFix.SessionSchedule(config), 0, logFactory, new QuickFix.DefaultMessageFactory(), "blah"); session.SetResponder(responder); session.CheckLatency = false; @@ -254,15 +254,15 @@ public bool RESENT() public bool SENT_REJECT() { - return responder.msgLookup.ContainsKey(QuickFix.Fields.MsgType.REJECT) && - responder.msgLookup[QuickFix.Fields.MsgType.REJECT].Count>0; + return responder.msgLookup.ContainsKey(QuickFix.Fields.MsgType.REJECT) && + responder.msgLookup[QuickFix.Fields.MsgType.REJECT].Count > 0; } - public bool SENT_HEART_BEAT() - { + public bool SENT_HEART_BEAT() + { return responder.msgLookup.ContainsKey(QuickFix.Fields.MsgType.HEARTBEAT) && responder.msgLookup[QuickFix.Fields.MsgType.HEARTBEAT].Count > 0; - } + } public bool SENT_BUSINESS_REJECT() { @@ -317,7 +317,7 @@ public bool SENT_REJECT(int reason, int refTag) QuickFix.Fields.SessionRejectReason reasonField = new QuickFix.Fields.SessionRejectReason(); msg.GetField(reasonField); - if(reasonField.getValue() != reason) + if (reasonField.getValue() != reason) return false; if (!msg.IsSetField(QuickFix.Fields.Tags.RefTagID)) @@ -373,7 +373,7 @@ private void SendTheMessage(QuickFix.Message msg) [Test] public void ConditionalTagMissingReject() - { + { application.fromAppException = new QuickFix.FieldNotFoundException(61); Logon(); @@ -388,10 +388,10 @@ public void ConditionalTagMissingReject() public void IncorrectTagValueReject() { application.fromAppException = new QuickFix.IncorrectTagValue(54); - + Logon(); SendNOSMessage(); - Assert.That(SENT_REJECT(QuickFix.Fields.SessionRejectReason.VALUE_IS_INCORRECT,54)); + Assert.That(SENT_REJECT(QuickFix.Fields.SessionRejectReason.VALUE_IS_INCORRECT, 54)); } @@ -408,7 +408,7 @@ public void UnsupportedMessageReject() [Test] public void LogonReject() { - application.fromAdminException = new QuickFix.RejectLogon("Failed Logon"); + application.fromAdminException = new QuickFix.RejectLogon("Failed Logon"); Logon(); Assert.That(SENT_LOGOUT()); @@ -505,7 +505,7 @@ public void TestGapFillOnResend() public void TestResendSessionLevelReject() { Assert.False(session.ResendSessionLevelRejects); // check for correct default - Logon(); + Logon(); QuickFix.FIX42.Reject reject = new QuickFix.FIX42.Reject( new QuickFix.Fields.RefSeqNum(10)); @@ -545,12 +545,12 @@ public void AssertMicrosecondsInTag(string msgType, int tag, bool shouldHaveMicr public void TestMillisecondsInSendingTimeStamp() { // MS in timestamp should default to Y - Assert.That(session.TimeStampPrecision == QuickFix.Fields.Converters.TimeStampPrecision.Millisecond ); + Assert.That(session.TimeStampPrecision == QuickFix.Fields.Converters.TimeStampPrecision.Millisecond); // Ms should show up Logon(); AssertMsInTag(QuickFix.Fields.MsgType.LOGON, QuickFix.Fields.Tags.SendingTime, true); - + // No ms session.TimeStampPrecision = QuickFix.Fields.Converters.TimeStampPrecision.Second; Logon(); @@ -585,7 +585,7 @@ public void TestMicrosecondsInSendingTimeStamp() // Less than FIX42 - no microseconds in timestamp, even if you tell it to sessionID = new QuickFix.SessionID(QuickFix.FixValues.BeginString.FIX40, "SENDER", "TARGET"); session.SessionID = sessionID; - session.TimeStampPrecision = QuickFix.Fields.Converters.TimeStampPrecision.Microsecond; + session.TimeStampPrecision = QuickFix.Fields.Converters.TimeStampPrecision.Microsecond; Logon40(); Assert.That(responder.msgLookup[QuickFix.Fields.MsgType.LOGON].Count == 3); AssertMicrosecondsInTag(QuickFix.Fields.MsgType.LOGON, QuickFix.Fields.Tags.SendingTime, false); @@ -595,11 +595,11 @@ public void TestMicrosecondsInSendingTimeStamp() public void TestMillisecondsInOrigSendingTimeStamp() { // MS in timestamp should default - Assert.That( session.TimeStampPrecision == QuickFix.Fields.Converters.TimeStampPrecision.Millisecond ); - + Assert.That(session.TimeStampPrecision == QuickFix.Fields.Converters.TimeStampPrecision.Millisecond); + // Logon first Logon(); - + // Do a resend request SendResendRequest(0, 2); AssertMsInTag(QuickFix.Fields.MsgType.SEQUENCERESET, QuickFix.Fields.Tags.OrigSendingTime, true); @@ -789,7 +789,7 @@ public void TestDoesSessionExist() QuickFix.SessionID validSessionID = new QuickFix.SessionID("FIX.4.2", "SENDER", "TARGET"); Assert.That(QuickFix.Session.DoesSessionExist(invalidSessionID), Is.EqualTo(false)); - Assert.That(QuickFix.Session.DoesSessionExist(validSessionID), Is.EqualTo(true)); + Assert.That(QuickFix.Session.DoesSessionExist(validSessionID), Is.EqualTo(true)); } [Test] @@ -845,7 +845,7 @@ public void TestRequiresOrigSendingTime_Y() Logon(); QuickFix.FIX42.SequenceReset sr = new QuickFix.FIX42.SequenceReset(new QuickFix.Fields.NewSeqNo(5)); - sr.GapFillFlag = new QuickFix.Fields.GapFillFlag(true); + sr.GapFillFlag = new QuickFix.Fields.GapFillFlag(false); sr.Header.SetField(new QuickFix.Fields.PossDupFlag(true)); sr.Header.SetField(new QuickFix.Fields.MsgSeqNum(seqNum--)); // so it triggers DoTargetTooLow code From 65d1c6c35161c546fbd1379154b9c9f7a585b57d Mon Sep 17 00:00:00 2001 From: Oliver Clancy Date: Tue, 25 Jul 2023 09:21:06 +0100 Subject: [PATCH 4/4] Add tests to demonstrate the issue and narrow the issue to resending msgs that reset the sequnece number with the same msgseqnum that caused the initial resendrequest --- QuickFIXn/Session.cs | 6 +- .../InitiatorSessionResendMessagesTest.cs | 213 ++++++++++++++++++ 2 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 UnitTests/InitiatorSessionResendMessagesTest.cs diff --git a/QuickFIXn/Session.cs b/QuickFIXn/Session.cs index 6ac8a2061..a44202297 100755 --- a/QuickFIXn/Session.cs +++ b/QuickFIXn/Session.cs @@ -926,7 +926,11 @@ protected void NextSequenceReset(Message sequenceReset) if (sequenceReset.IsSetField(Fields.Tags.GapFillFlag)) isGapFill = sequenceReset.GetBoolean(Fields.Tags.GapFillFlag); - if (!Verify(sequenceReset, !isGapFill, !isGapFill)) + bool possDupFlag = false; + if (sequenceReset.Header.IsSetField(Fields.Tags.PossDupFlag)) + possDupFlag = sequenceReset.Header.GetBoolean(Fields.Tags.PossDupFlag); + + if (!Verify(sequenceReset, isGapFill, isGapFill && !possDupFlag)) return; if (sequenceReset.IsSetField(Fields.Tags.NewSeqNo)) diff --git a/UnitTests/InitiatorSessionResendMessagesTest.cs b/UnitTests/InitiatorSessionResendMessagesTest.cs new file mode 100644 index 000000000..69a0246fd --- /dev/null +++ b/UnitTests/InitiatorSessionResendMessagesTest.cs @@ -0,0 +1,213 @@ +using NUnit.Framework; +using QuickFix.Fields; +using QuickFix.FIX42; +using System.Text.RegularExpressions; + +namespace UnitTests +{ + + [TestFixture] + public class InitiatorSessionResendMessagesTest + { + MockResponder responder = null; + + QuickFix.SessionID sessionID = null; + QuickFix.SessionSettings settings = null; + MockApplication application = null; + QuickFix.Session session = null; + QuickFix.Dictionary config = null; + SeqNumType seqNum = 1; + Regex msRegex = new Regex(@"\.[\d]{1,3}$"); + Regex microsecondRegex = new Regex(@"\.[\d]{1,6}$"); + + [SetUp] + public void Setup() + { + responder = new MockResponder(); + sessionID = new QuickFix.SessionID("FIX.4.2", "SENDER", "TARGET"); + application = new MockApplication(); + settings = new QuickFix.SessionSettings(); + + config = new QuickFix.Dictionary(); + config.SetBool(QuickFix.SessionSettings.PERSIST_MESSAGES, false); + config.SetBool(QuickFix.SessionSettings.RESETSEQUENCE_MESSAGE_REQUIRES_ORIGSENDINGTIME, false); + config.SetString(QuickFix.SessionSettings.CONNECTION_TYPE, "initiator"); + config.SetString(QuickFix.SessionSettings.START_TIME, "00:00:00"); + config.SetString(QuickFix.SessionSettings.END_TIME, "00:00:00"); + settings.Set(sessionID, config); + + // initiator + session = new QuickFix.Session(true, application, new QuickFix.MemoryStoreFactory(), sessionID, + new QuickFix.DataDictionaryProvider(), new QuickFix.SessionSchedule(config), 0, new QuickFix.ScreenLogFactory(settings), new QuickFix.DefaultMessageFactory(), "blah"); + session.SetResponder(responder); + session.CheckLatency = false; + + seqNum = 1; + } + + public void Logon() + { + SendLogon(new QuickFix.FIX42.Logon()); + } + + private void SendLogon(QuickFix.Message msg) + { + msg.Header.SetField(new QuickFix.Fields.TargetCompID(sessionID.SenderCompID)); + msg.Header.SetField(new QuickFix.Fields.SenderCompID(sessionID.TargetCompID)); + msg.Header.SetField(new QuickFix.Fields.MsgSeqNum(seqNum++)); + msg.Header.SetField(new QuickFix.Fields.SendingTime(System.DateTime.UtcNow)); + msg.SetField(new QuickFix.Fields.HeartBtInt(1)); + session.Next(msg.ToString()); + } + + + public bool RESENT() + { + if (responder.dups.Count == 0) + return false; + + responder.dups.Dequeue(); + return true; + } + + + public void SendResendRequest(SeqNumType begin, SeqNumType end) + { + SendTheMessage(new QuickFix.FIX42.ResendRequest( + new QuickFix.Fields.BeginSeqNo(begin), + new QuickFix.Fields.EndSeqNo(end))); + } + + private void SendTheMessage(QuickFix.Message msg) + { + SendTheMessage(msg, seqNum++); + } + + private void SendTheMessage(QuickFix.Message msg, SeqNumType msgSeqNum, bool possDupFlag=false) + { + msg.Header.SetField(new QuickFix.Fields.TargetCompID(sessionID.SenderCompID)); + msg.Header.SetField(new QuickFix.Fields.SenderCompID(sessionID.TargetCompID)); + msg.Header.SetField(new QuickFix.Fields.MsgSeqNum(msgSeqNum)); + msg.Header.SetField(new QuickFix.Fields.PossDupFlag(possDupFlag)); + session.Next(msg.ToString()); + } + + [Test] + public void TestSequenceResetNoGapFillIsProcessed() + { + // Default is false + Assert.That(session.IgnorePossDupResendRequests, Is.EqualTo(false)); + + session.Next();// causes Logon send + // Logon + Logon(); + + // quote + var quote = new QuickFix.FIX42.Quote(); + + + SendTheMessage(quote); + SendTheMessage(quote); + SendTheMessage(quote); + + var seqReset = new SequenceReset( new NewSeqNo(seqNum + 10) ); + + SendTheMessage(seqReset); + + + Assert.That(session.NextTargetMsgSeqNum, Is.EqualTo(seqNum +9 )); + } + + [Test] + public void TestSequenceResetWithGapFillIsProcessed() + { + // Default is false + Assert.That(session.IgnorePossDupResendRequests, Is.EqualTo(false)); + + session.Next();// causes Logon send + // Logon + Logon(); + + // quote + var quote = new QuickFix.FIX42.Quote(); + + + SendTheMessage(quote); + SendTheMessage(quote); + SendTheMessage(quote); + + var seqReset = new SequenceReset(new NewSeqNo(seqNum + 10)); + seqReset.GapFillFlag = new GapFillFlag(true); + + SendTheMessage(seqReset); + + Assert.That(session.NextTargetMsgSeqNum, Is.EqualTo(seqNum + 9)); + } + + [Test] + public void TestSequenceResetDuringResendRequestIsProcessed() + { + // Default is false + Assert.That(session.IgnorePossDupResendRequests, Is.EqualTo(false)); + session.RequiresOrigSendingTime = false; + session.Next(); // causes Logon send + // Logon + Logon(); + + // quote + var quote = new QuickFix.FIX42.Quote(); + + + SendTheMessage(quote, 2); + SendTheMessage(quote, 3); + SendTheMessage(quote, 10); // causes ResendRequest + + Assert.That(session.NextTargetMsgSeqNum, Is.EqualTo(4)); + + SendTheMessage(quote, 4); + SendTheMessage(quote, 5); + + var seqReset = new SequenceReset(new NewSeqNo(9)); // already been sent 10 so will process it + + SendTheMessage(seqReset, 6); + SendTheMessage(quote, 9); + + Assert.That(session.NextTargetMsgSeqNum, Is.EqualTo(11)); + } + + [Test] + public void TestSequenceResetWithGapFillAndPossDupFlagTrueDuringResendRequestIsProcessed() + { + // Default is false + Assert.That(session.IgnorePossDupResendRequests, Is.EqualTo(false)); + session.RequiresOrigSendingTime = false; + session.Next(); // causes Logon send + // Logon + Logon(); + + // quote + var quote = new QuickFix.FIX42.Quote(); + + SendTheMessage(quote, 2); + SendTheMessage(quote, 3); + SendTheMessage(quote, 4); + + seqNum = 10; + // without changes the arrowed comments scenario happens + Logon(); // <--- Will cause resend from 5 and is queued (all cases) + + SendTheMessage(quote, 5, true); + SendTheMessage(quote, 6, true); + SendTheMessage(quote, 7, true); + SendTheMessage(quote, 8, true); + SendTheMessage(quote, 9, true); // <--will cause logon message queued above to be replayed AND advance msgSeqNum to 11! (all cases) + var seqReset = new SequenceReset(new NewSeqNo(12)); + seqReset.GapFillFlag = new GapFillFlag(true); + SendTheMessage(seqReset, 10, true); // <-- Without change this message will be ignored as too low but poss dup is Y so will NOT throw - in the changed version this WILL be honored + + Assert.That(session.NextTargetMsgSeqNum, Is.EqualTo(12)); + + Assert.That(responder.msgLookup.ContainsKey(QuickFix.Fields.MsgType.RESEND_REQUEST) && responder.msgLookup[QuickFix.Fields.MsgType.RESEND_REQUEST].Count == 1); + } + } +}