diff --git a/QuickFIXn/Session.cs b/QuickFIXn/Session.cs index 128dae377..a44202297 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,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)) @@ -1058,7 +1062,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 +1154,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 +1392,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 +1537,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 +1599,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/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); + } + } +} 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