|
15 | 15 | #include "databento/datetime.hpp" |
16 | 16 | #include "databento/enums.hpp" // Schema, SType |
17 | 17 | #include "databento/exceptions.hpp" |
| 18 | +#include "databento/ireadable.hpp" |
18 | 19 | #include "databento/live.hpp" |
19 | 20 | #include "databento/live_blocking.hpp" |
20 | 21 | #include "databento/live_subscription.hpp" |
@@ -765,4 +766,149 @@ TEST_F(LiveBlockingTests, TestHeartbeatTimeoutOnNextRecordWithTimeout) { |
765 | 766 | EXPECT_TRUE(got_timeout_exception) << "Expected heartbeat timeout exception"; |
766 | 767 | } |
767 | 768 |
|
| 769 | +TEST_F(LiveBlockingTests, TestTryNextRecordEmptyBuffer) { |
| 770 | + constexpr auto kTsOut = false; |
| 771 | + const mock::MockLsgServer mock_server{ |
| 772 | + dataset::kXnasItch, kTsOut, [](mock::MockLsgServer& self) { |
| 773 | + self.Accept(); |
| 774 | + self.Authenticate(); |
| 775 | + std::this_thread::sleep_for(std::chrono::milliseconds{200}); |
| 776 | + }}; |
| 777 | + |
| 778 | + LiveBlocking target = builder_.SetDataset(dataset::kXnasItch) |
| 779 | + .SetSendTsOut(kTsOut) |
| 780 | + .SetAddress(kLocalhost, mock_server.Port()) |
| 781 | + .BuildBlocking(); |
| 782 | + // Buffer is empty, no I/O should be performed |
| 783 | + const auto* rec = target.TryNextRecord(); |
| 784 | + EXPECT_EQ(rec, nullptr); |
| 785 | +} |
| 786 | + |
| 787 | +TEST_F(LiveBlockingTests, TestTryNextRecordAfterFillBuffer) { |
| 788 | + constexpr auto kTsOut = false; |
| 789 | + constexpr OhlcvMsg kRec{DummyHeader<OhlcvMsg>(RType::Ohlcv1M), 1, 2, 3, 4, 5}; |
| 790 | + const mock::MockLsgServer mock_server{dataset::kXnasItch, kTsOut, |
| 791 | + [kRec](mock::MockLsgServer& self) { |
| 792 | + self.Accept(); |
| 793 | + self.Authenticate(); |
| 794 | + self.SendRecord(kRec); |
| 795 | + }}; |
| 796 | + |
| 797 | + LiveBlocking target = builder_.SetDataset(dataset::kXnasItch) |
| 798 | + .SetSendTsOut(kTsOut) |
| 799 | + .SetAddress(kLocalhost, mock_server.Port()) |
| 800 | + .BuildBlocking(); |
| 801 | + const auto fill_res = target.FillBuffer(std::chrono::milliseconds{1000}); |
| 802 | + ASSERT_EQ(fill_res.status, IReadable::Status::Ok); |
| 803 | + ASSERT_GT(fill_res.read_size, 0); |
| 804 | + const auto* rec = target.TryNextRecord(); |
| 805 | + ASSERT_NE(rec, nullptr); |
| 806 | + ASSERT_TRUE(rec->Holds<OhlcvMsg>()); |
| 807 | + EXPECT_EQ(rec->Get<OhlcvMsg>(), kRec); |
| 808 | + // Buffer drained |
| 809 | + EXPECT_EQ(target.TryNextRecord(), nullptr); |
| 810 | +} |
| 811 | + |
| 812 | +TEST_F(LiveBlockingTests, TestFillBufferReturnsClosed) { |
| 813 | + constexpr auto kTsOut = false; |
| 814 | + const mock::MockLsgServer mock_server{dataset::kXnasItch, kTsOut, |
| 815 | + [](mock::MockLsgServer& self) { |
| 816 | + self.Accept(); |
| 817 | + self.Authenticate(); |
| 818 | + self.Close(); |
| 819 | + }}; |
| 820 | + |
| 821 | + LiveBlocking target = builder_.SetDataset(dataset::kXnasItch) |
| 822 | + .SetSendTsOut(kTsOut) |
| 823 | + .SetAddress(kLocalhost, mock_server.Port()) |
| 824 | + .BuildBlocking(); |
| 825 | + const auto fill_res = target.FillBuffer(std::chrono::milliseconds{1000}); |
| 826 | + EXPECT_EQ(fill_res.status, IReadable::Status::Closed); |
| 827 | + EXPECT_EQ(fill_res.read_size, 0); |
| 828 | +} |
| 829 | + |
| 830 | +TEST_F(LiveBlockingTests, TestTryNextRecordPollLoop) { |
| 831 | + constexpr auto kTsOut = false; |
| 832 | + constexpr auto kRecCount = 5; |
| 833 | + constexpr OhlcvMsg kRec{DummyHeader<OhlcvMsg>(RType::Ohlcv1M), 1, 2, 3, 4, 5}; |
| 834 | + const mock::MockLsgServer mock_server{dataset::kXnasItch, kTsOut, |
| 835 | + [kRec](mock::MockLsgServer& self) { |
| 836 | + self.Accept(); |
| 837 | + self.Authenticate(); |
| 838 | + for (size_t i = 0; i < kRecCount; ++i) { |
| 839 | + self.SendRecord(kRec); |
| 840 | + } |
| 841 | + self.Close(); |
| 842 | + }}; |
| 843 | + |
| 844 | + LiveBlocking target = builder_.SetDataset(dataset::kXnasItch) |
| 845 | + .SetSendTsOut(kTsOut) |
| 846 | + .SetAddress(kLocalhost, mock_server.Port()) |
| 847 | + .BuildBlocking(); |
| 848 | + int record_count = 0; |
| 849 | + while (true) { |
| 850 | + while (const auto* rec = target.TryNextRecord()) { |
| 851 | + ASSERT_TRUE(rec->Holds<OhlcvMsg>()); |
| 852 | + EXPECT_EQ(rec->Get<OhlcvMsg>(), kRec); |
| 853 | + ++record_count; |
| 854 | + } |
| 855 | + const auto fill_res = target.FillBuffer(std::chrono::milliseconds{1000}); |
| 856 | + if (fill_res.status == IReadable::Status::Closed) { |
| 857 | + break; |
| 858 | + } |
| 859 | + } |
| 860 | + EXPECT_EQ(record_count, kRecCount); |
| 861 | +} |
| 862 | + |
| 863 | +TEST_F(LiveBlockingTests, TestTryNextRecordPartialRecord) { |
| 864 | + constexpr auto kTsOut = false; |
| 865 | + constexpr MboMsg kRec{DummyHeader<MboMsg>(RType::Mbo), |
| 866 | + 1, |
| 867 | + 2, |
| 868 | + 3, |
| 869 | + {}, |
| 870 | + 4, |
| 871 | + Action::Add, |
| 872 | + Side::Bid, |
| 873 | + UnixNanos{}, |
| 874 | + TimeDeltaNanos{}, |
| 875 | + 100}; |
| 876 | + |
| 877 | + bool send_remaining{}; |
| 878 | + std::mutex send_remaining_mutex; |
| 879 | + std::condition_variable send_remaining_cv; |
| 880 | + const mock::MockLsgServer mock_server{ |
| 881 | + dataset::kGlbxMdp3, kTsOut, |
| 882 | + [kRec, &send_remaining, &send_remaining_mutex, |
| 883 | + &send_remaining_cv](mock::MockLsgServer& self) { |
| 884 | + self.Accept(); |
| 885 | + self.Authenticate(); |
| 886 | + self.SplitSendRecord(kRec, send_remaining, send_remaining_mutex, |
| 887 | + send_remaining_cv); |
| 888 | + }}; |
| 889 | + |
| 890 | + LiveBlocking target = builder_.SetDataset(dataset::kGlbxMdp3) |
| 891 | + .SetSendTsOut(kTsOut) |
| 892 | + .SetAddress(kLocalhost, mock_server.Port()) |
| 893 | + .BuildBlocking(); |
| 894 | + // Read partial record (just header) |
| 895 | + auto fill_res = target.FillBuffer(std::chrono::milliseconds{1000}); |
| 896 | + ASSERT_EQ(fill_res.status, IReadable::Status::Ok); |
| 897 | + // Record is incomplete |
| 898 | + EXPECT_EQ(target.TryNextRecord(), nullptr); |
| 899 | + // Signal server to send remaining bytes |
| 900 | + { |
| 901 | + const std::lock_guard<std::mutex> lock{send_remaining_mutex}; |
| 902 | + send_remaining = true; |
| 903 | + send_remaining_cv.notify_one(); |
| 904 | + } |
| 905 | + // Read the rest |
| 906 | + fill_res = target.FillBuffer(std::chrono::milliseconds{1000}); |
| 907 | + ASSERT_EQ(fill_res.status, IReadable::Status::Ok); |
| 908 | + const auto* rec = target.TryNextRecord(); |
| 909 | + ASSERT_NE(rec, nullptr); |
| 910 | + ASSERT_TRUE(rec->Holds<MboMsg>()); |
| 911 | + EXPECT_EQ(rec->Get<MboMsg>(), kRec); |
| 912 | +} |
| 913 | + |
768 | 914 | } // namespace databento::tests |
0 commit comments