|
16 | 16 | package quickfix |
17 | 17 |
|
18 | 18 | import ( |
| 19 | + "bytes" |
19 | 20 | "testing" |
20 | 21 | "time" |
21 | 22 |
|
22 | | - "github.com/stretchr/testify/mock" |
23 | 23 | "github.com/stretchr/testify/suite" |
24 | 24 |
|
25 | 25 | "github.com/quickfixgo/quickfix/internal" |
@@ -374,23 +374,84 @@ func (s *InSessionTestSuite) TestFIXMsgInResendRequestDoNotSendApp() { |
374 | 374 | s.State(inSession{}) |
375 | 375 | } |
376 | 376 |
|
377 | | -func (s *InSessionTestSuite) TestFIXMsgInResendRequestBlocksSend() { |
| 377 | +// resendInterceptStore delegates to a real store but hooks IterateMessages so a |
| 378 | +// test can simulate an application goroutine sending live traffic mid-replay. |
| 379 | +type resendInterceptStore struct { |
| 380 | + MessageStore |
| 381 | + afterFirstMsg func() |
| 382 | +} |
| 383 | + |
| 384 | +func (s *resendInterceptStore) IterateMessages(beginSeqNum, endSeqNum int, cb func([]byte) error) error { |
| 385 | + n := 0 |
| 386 | + return s.MessageStore.IterateMessages(beginSeqNum, endSeqNum, func(msgBytes []byte) error { |
| 387 | + err := cb(msgBytes) |
| 388 | + if n++; n == 1 && s.afterFirstMsg != nil { |
| 389 | + s.afterFirstMsg() |
| 390 | + } |
| 391 | + return err |
| 392 | + }) |
| 393 | +} |
| 394 | + |
| 395 | +func (s *InSessionTestSuite) TestFIXMsgInResendRequestLiveMessagesDoNotInterleave() { |
378 | 396 | s.MockApp.On("ToApp").Return(nil) |
379 | | - s.Require().Nil(s.session.send(s.NewOrderSingle())) |
380 | | - s.LastToAppMessageSent() |
381 | | - s.MockApp.AssertNumberOfCalls(s.T(), "ToApp", 1) |
382 | | - s.NextSenderMsgSeqNum(2) |
| 397 | + for i := 0; i < 3; i++ { |
| 398 | + s.Require().Nil(s.session.send(s.NewOrderSingle())) |
| 399 | + } |
| 400 | + s.NextSenderMsgSeqNum(4) |
| 401 | + for { |
| 402 | + if msgBytes, _ := s.Receiver.LastMessage(); msgBytes == nil { |
| 403 | + break |
| 404 | + } |
| 405 | + } |
383 | 406 |
|
384 | | - s.MockStore.On("IterateMessages", mock.Anything, mock.Anything, mock.AnythingOfType("func([]byte) error")). |
385 | | - Run(func(_ mock.Arguments) { |
386 | | - s.Require().Nil(s.session.send(s.NewOrderSingle())) |
387 | | - }). |
388 | | - Return(nil) |
| 407 | + // While the replay is in progress, an application goroutine sends a live |
| 408 | + // message the same way SendToTarget does. The send must block until the |
| 409 | + // replay completes so the message cannot reach the wire mid-replay. |
| 410 | + liveSent := make(chan error, 1) |
| 411 | + s.session.store = &resendInterceptStore{ |
| 412 | + MessageStore: &s.MockStore, |
| 413 | + afterFirstMsg: func() { |
| 414 | + go func() { |
| 415 | + liveSent <- s.session.queueForSend(s.NewOrderSingle()) |
| 416 | + }() |
| 417 | + select { |
| 418 | + case err := <-liveSent: |
| 419 | + s.Failf("", "live send completed during replay (err=%v)", err) |
| 420 | + case <-time.After(20 * time.Millisecond): |
| 421 | + } |
| 422 | + }, |
| 423 | + } |
389 | 424 |
|
390 | 425 | s.MockApp.On("FromAdmin").Return(nil) |
391 | 426 | s.fixMsgIn(s.session, s.ResendRequest(1)) |
392 | 427 |
|
393 | | - s.NextSenderMsgSeqNum(2) |
| 428 | + select { |
| 429 | + case err := <-liveSent: |
| 430 | + s.Require().Nil(err) |
| 431 | + case <-time.After(5 * time.Second): |
| 432 | + s.FailNow("live send never completed after the replay finished") |
| 433 | + } |
| 434 | + |
| 435 | + for expectedSeqNum := 1; expectedSeqNum <= 3; expectedSeqNum++ { |
| 436 | + msgBytes, _ := s.Receiver.LastMessage() |
| 437 | + s.Require().NotNil(msgBytes) |
| 438 | + msg := NewMessage() |
| 439 | + s.Require().Nil(ParseMessage(msg, bytes.NewBuffer(msgBytes))) |
| 440 | + s.FieldEquals(tagMsgSeqNum, expectedSeqNum, msg.Header) |
| 441 | + s.FieldEquals(tagPossDupFlag, true, msg.Header) |
| 442 | + } |
| 443 | + msgBytes, _ := s.Receiver.LastMessage() |
| 444 | + s.Nil(msgBytes, "live message must not be sent during the replay") |
| 445 | + |
| 446 | + // The live message stays queued and goes out once the event loop resumes. |
| 447 | + s.session.SendAppMessages(s.session) |
| 448 | + msgBytes, _ = s.Receiver.LastMessage() |
| 449 | + s.Require().NotNil(msgBytes) |
| 450 | + msg := NewMessage() |
| 451 | + s.Require().Nil(ParseMessage(msg, bytes.NewBuffer(msgBytes))) |
| 452 | + s.FieldEquals(tagMsgSeqNum, 4, msg.Header) |
| 453 | + s.False(msg.Header.Has(tagPossDupFlag)) |
| 454 | + s.NextSenderMsgSeqNum(5) |
394 | 455 | } |
395 | 456 |
|
396 | 457 | func (s *InSessionTestSuite) TestFIXMsgInTargetTooLow() { |
|
0 commit comments