| // Copyright 2026 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "net/socket/delayed_stream_socket.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <vector> |
| |
| #include "base/test/task_environment.h" |
| #include "base/test/test_future.h" |
| #include "base/time/time.h" |
| #include "net/base/ip_address.h" |
| #include "net/base/ip_endpoint.h" |
| #include "net/base/net_errors.h" |
| #include "net/base/test_completion_callback.h" |
| #include "net/socket/bandwidth_throttle.h" |
| #include "net/socket/next_proto.h" |
| #include "net/socket/socket_tag.h" |
| #include "net/socket/socket_test_util.h" |
| #include "net/ssl/ssl_info.h" |
| #include "net/test/gtest_util.h" |
| #include "net/test/test_with_task_environment.h" |
| #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace net { |
| namespace { |
| |
| using net::test::IsError; |
| using net::test::IsOk; |
| using ::testing::AnyOf; |
| |
| constexpr base::TimeDelta kLatency = base::Milliseconds(150); |
| class DelayedStreamSocketTest : public testing::Test, |
| public WithTaskEnvironment { |
| public: |
| DelayedStreamSocketTest() |
| : WithTaskEnvironment( |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME) {} |
| |
| protected: |
| // Creates a DelayedStreamSocket wrapping a MockTCPClientSocket with the |
| // given data provider and config. Throttles default to null (latency-only). |
| std::unique_ptr<DelayedStreamSocket> CreateSocket( |
| SocketDataProvider* data_provider, |
| const DelayedSocketConfig& config, |
| scoped_refptr<BandwidthThrottle> download_throttle = nullptr, |
| scoped_refptr<BandwidthThrottle> upload_throttle = nullptr) { |
| auto tcp_socket = std::make_unique<MockTCPClientSocket>( |
| AddressList(IPEndPoint(IPAddress::IPv4Localhost(), 80)), |
| /*net_log=*/nullptr, data_provider); |
| return std::make_unique<DelayedStreamSocket>(std::move(tcp_socket), config, |
| std::move(download_throttle), |
| std::move(upload_throttle)); |
| } |
| |
| DelayedSocketConfig MakeConfig( |
| base::TimeDelta rtt = kLatency, |
| std::optional<uint64_t> download_bytes_per_sec = std::nullopt, |
| std::optional<uint64_t> upload_bytes_per_sec = std::nullopt) { |
| return {.rtt = rtt, |
| .download_throughput_bytes_per_sec = download_bytes_per_sec, |
| .upload_throughput_bytes_per_sec = upload_bytes_per_sec}; |
| } |
| |
| DelayedSocketConfig NoDelayConfig() { return {.rtt = base::TimeDelta()}; } |
| }; |
| |
| // --- Connect tests --- |
| |
| TEST_F(DelayedStreamSocketTest, ConnectAsyncWithLatency) { |
| SequencedSocketData data_provider; |
| data_provider.set_connect_data(MockConnect(ASYNC, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> callback; |
| EXPECT_THAT(socket->Connect(callback.GetCallback()), |
| test::IsError(ERR_IO_PENDING)); |
| |
| // The inner socket completes async, then latency timer starts. |
| // Advance to just before latency expires: should not be ready yet. |
| FastForwardBy(kLatency - base::Milliseconds(1)); |
| EXPECT_FALSE(callback.IsReady()); |
| |
| FastForwardBy(base::Milliseconds(1)); |
| EXPECT_TRUE(callback.IsReady()); |
| EXPECT_THAT(callback.Get(), test::IsOk()); |
| } |
| |
| TEST_F(DelayedStreamSocketTest, ConnectSyncWithLatency) { |
| SequencedSocketData data_provider; |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> callback; |
| // Sync completion converted to async due to latency. |
| EXPECT_THAT(socket->Connect(callback.GetCallback()), |
| test::IsError(ERR_IO_PENDING)); |
| |
| EXPECT_FALSE(callback.IsReady()); |
| FastForwardBy(kLatency); |
| EXPECT_TRUE(callback.IsReady()); |
| EXPECT_THAT(callback.Get(), test::IsOk()); |
| } |
| |
| TEST_F(DelayedStreamSocketTest, ConnectSyncError) { |
| SequencedSocketData data_provider; |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, ERR_FAILED)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> callback; |
| // Even errors get delayed (simulates the RTT it takes to learn the |
| // connection failed, e.g. RST). |
| EXPECT_THAT(socket->Connect(callback.GetCallback()), |
| test::IsError(ERR_IO_PENDING)); |
| |
| FastForwardBy(kLatency); |
| EXPECT_THAT(callback.Get(), test::IsError(ERR_FAILED)); |
| } |
| |
| TEST_F(DelayedStreamSocketTest, ConnectNoLatencyPassthrough) { |
| SequencedSocketData data_provider; |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, NoDelayConfig()); |
| |
| base::test::TestFuture<int> callback; |
| // With no latency, sync completion passes through. |
| EXPECT_THAT(socket->Connect(callback.GetCallback()), test::IsOk()); |
| } |
| |
| // --- Read tests --- |
| |
| TEST_F(DelayedStreamSocketTest, ReadPaysHalfRtt) { |
| const std::string kData = "hello"; // 5 bytes |
| MockRead reads[] = { |
| MockRead(SYNCHRONOUS, kData), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| // Use unlimited throughput so the buffer only applies latency. |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/std::nullopt)); |
| |
| // Connect first (skip latency for simplicity). |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), test::IsOk()); |
| |
| // Read: should pay half-RTT latency only. |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), 1024, read_cb.GetCallback()), |
| test::IsError(ERR_IO_PENDING)); |
| |
| // Expected delay: half-RTT (75ms). |
| FastForwardBy(base::Milliseconds(74)); |
| EXPECT_FALSE(read_cb.IsReady()); |
| |
| FastForwardBy(base::Milliseconds(1)); |
| EXPECT_TRUE(read_cb.IsReady()); |
| EXPECT_EQ(read_cb.Get(), static_cast<int>(kData.size())); |
| } |
| |
| TEST_F(DelayedStreamSocketTest, SequentialReadsSkipHalfRtt) { |
| const std::string kData1 = "hello"; // 5 bytes |
| const std::string kData2 = "world"; // 5 bytes |
| MockRead reads[] = { |
| MockRead(SYNCHRONOUS, kData1), |
| MockRead(SYNCHRONOUS, kData2), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| // Use unlimited throughput so the buffer only applies latency. |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/std::nullopt)); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), test::IsOk()); |
| |
| // First read; half-RTT latency. Limit the read to the first chunk's size |
| // so BottleneckBuffer::Pull (which concatenates ready chunks across |
| // boundaries in kStream mode) does not merge in the read-ahead second |
| // chunk. |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb1; |
| socket->Read(buffer.get(), static_cast<int>(kData1.size()), |
| read_cb1.GetCallback()); |
| // half-RTT = 75ms |
| FastForwardBy(base::Milliseconds(75)); |
| ASSERT_EQ(read_cb1.Get(), 5); |
| |
| // Second read issued immediately after first completes. The read-ahead |
| // already buffered it and its latency elapsed in parallel, so it completes |
| // synchronously. |
| base::test::TestFuture<int> read_cb2; |
| int rv = socket->Read(buffer.get(), 1024, read_cb2.GetCallback()); |
| EXPECT_EQ(rv, 5); |
| } |
| |
| // --- Write tests --- |
| |
| TEST_F(DelayedStreamSocketTest, WritePaysHalfRtt) { |
| const std::string kData = "hello"; // 5 bytes |
| MockWrite writes[] = { |
| MockWrite(SYNCHRONOUS, kData), |
| }; |
| StaticSocketDataProvider data_provider(base::span<MockRead>(), writes); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| // Use unlimited throughput so the buffer only applies latency. |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/std::nullopt)); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), test::IsOk()); |
| |
| auto buffer = base::MakeRefCounted<StringIOBuffer>(kData); |
| base::test::TestFuture<int> write_cb; |
| EXPECT_THAT(socket->Write(buffer.get(), kData.size(), write_cb.GetCallback(), |
| TRAFFIC_ANNOTATION_FOR_TESTS), |
| test::IsError(ERR_IO_PENDING)); |
| |
| // Expected delay: half-RTT (75ms) only. |
| FastForwardBy(base::Milliseconds(74)); |
| EXPECT_FALSE(write_cb.IsReady()); |
| |
| FastForwardBy(base::Milliseconds(1)); |
| EXPECT_TRUE(write_cb.IsReady()); |
| EXPECT_EQ(write_cb.Get(), static_cast<int>(kData.size())); |
| } |
| |
| TEST_F(DelayedStreamSocketTest, WriteReadCycleIsOneRtt) { |
| // Verify that Write + Read (with a gap between them) totals one full RTT. |
| const std::string kRequest = "GET /"; |
| const std::string kResponse = "HTTP"; |
| MockWrite writes[] = { |
| MockWrite(SYNCHRONOUS, kRequest), |
| }; |
| MockRead reads[] = { |
| MockRead(SYNCHRONOUS, kResponse), |
| }; |
| StaticSocketDataProvider data_provider(reads, writes); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| // Latency-only config (no throughput) to isolate RTT. |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/std::nullopt)); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), test::IsOk()); |
| |
| base::TimeTicks start = base::TimeTicks::Now(); |
| |
| // Write: half-RTT (first write, no sequence). |
| auto write_buffer = base::MakeRefCounted<StringIOBuffer>(kRequest); |
| base::test::TestFuture<int> write_cb; |
| EXPECT_THAT( |
| socket->Write(write_buffer.get(), kRequest.size(), write_cb.GetCallback(), |
| TRAFFIC_ANNOTATION_FOR_TESTS), |
| test::IsError(ERR_IO_PENDING)); |
| FastForwardBy(kLatency / 2); |
| ASSERT_EQ(write_cb.Get(), static_cast<int>(kRequest.size())); |
| |
| // Small gap (> kSequenceThreshold) so the Read is not part of the |
| // write sequence. Since reads and writes are tracked independently, |
| // this Read is the first read and pays half-RTT. |
| FastForwardBy(base::Milliseconds(2)); |
| |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), 1024, read_cb.GetCallback()), |
| test::IsError(ERR_IO_PENDING)); |
| FastForwardBy(kLatency / 2); |
| ASSERT_EQ(read_cb.Get(), static_cast<int>(kResponse.size())); |
| |
| // Total: half-RTT (write) + 2ms gap + half-RTT (read) ~= 1 RTT + 2ms. |
| base::TimeDelta elapsed = base::TimeTicks::Now() - start; |
| EXPECT_EQ(elapsed, kLatency + base::Milliseconds(2)); |
| } |
| |
| TEST_F(DelayedStreamSocketTest, ReadAfterGapReturnsBufferedDataSynchronously) { |
| // The stream socket reads ahead from the underlying socket. After the |
| // first read completes, the second buffered chunk has already paid its |
| // half-RTT in parallel, so a later Read() returns it synchronously; just |
| // like a real TCP receive buffer that already holds the next segment. |
| const std::string kData1 = "hello"; |
| const std::string kData2 = "world"; |
| MockRead reads[] = { |
| MockRead(SYNCHRONOUS, kData1), |
| MockRead(SYNCHRONOUS, kData2), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/std::nullopt)); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), test::IsOk()); |
| |
| // First read: half-RTT. Cap the byte count to the first chunk's length so |
| // the stream-mode Pull does not concatenate the already-buffered second |
| // chunk into this read's result. |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb1; |
| socket->Read(buffer.get(), static_cast<int>(kData1.size()), |
| read_cb1.GetCallback()); |
| FastForwardBy(kLatency / 2); |
| ASSERT_EQ(read_cb1.Get(), 5); |
| |
| // Even after a gap the second chunk is already in the buffer. |
| FastForwardBy(base::Milliseconds(10)); |
| |
| base::test::TestFuture<int> read_cb2; |
| EXPECT_EQ(socket->Read(buffer.get(), 1024, read_cb2.GetCallback()), 5); |
| } |
| |
| // --- Passthrough with no delay --- |
| |
| TEST_F(DelayedStreamSocketTest, NoDelayActsAsPassthrough) { |
| const std::string kData = "hello"; |
| MockRead reads[] = { |
| MockRead(SYNCHRONOUS, 0, kData), |
| }; |
| MockWrite writes[] = { |
| MockWrite(SYNCHRONOUS, 1, kData), |
| }; |
| SequencedSocketData data_provider(reads, writes); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, NoDelayConfig()); |
| |
| // Connect: synchronous passthrough. |
| base::test::TestFuture<int> connect_cb; |
| EXPECT_THAT(socket->Connect(connect_cb.GetCallback()), test::IsOk()); |
| |
| // Read: synchronous passthrough. |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_EQ(socket->Read(buffer.get(), 1024, read_cb.GetCallback()), |
| static_cast<int>(kData.size())); |
| |
| // Write: synchronous passthrough. |
| auto write_buffer = base::MakeRefCounted<StringIOBuffer>(kData); |
| base::test::TestFuture<int> write_cb; |
| EXPECT_EQ(socket->Write(write_buffer.get(), kData.size(), |
| write_cb.GetCallback(), TRAFFIC_ANNOTATION_FOR_TESTS), |
| static_cast<int>(kData.size())); |
| } |
| |
| // --- Lifetime safety --- |
| |
| TEST_F(DelayedStreamSocketTest, DestroyDuringPendingConnect) { |
| SequencedSocketData data_provider; |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> callback; |
| EXPECT_THAT(socket->Connect(callback.GetCallback()), |
| test::IsError(ERR_IO_PENDING)); |
| |
| // Destroy socket while connect delay is pending. |
| socket.reset(); |
| |
| // Advance time past the latency: callback should NOT fire. |
| FastForwardBy(kLatency * 2); |
| EXPECT_FALSE(callback.IsReady()); |
| } |
| |
| TEST_F(DelayedStreamSocketTest, DestroyDuringPendingRead) { |
| const std::string kData = "hello"; |
| MockRead reads[] = { |
| MockRead(SYNCHRONOUS, kData), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), test::IsOk()); |
| |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), 1024, read_cb.GetCallback()), |
| test::IsError(ERR_IO_PENDING)); |
| |
| // Destroy socket while read delay is pending. |
| socket.reset(); |
| |
| FastForwardBy(kLatency * 2); |
| EXPECT_FALSE(read_cb.IsReady()); |
| } |
| |
| TEST_F(DelayedStreamSocketTest, DisconnectCancelsPendingOperations) { |
| SequencedSocketData data_provider; |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> callback; |
| EXPECT_THAT(socket->Connect(callback.GetCallback()), |
| test::IsError(ERR_IO_PENDING)); |
| |
| socket->Disconnect(); |
| |
| FastForwardBy(kLatency * 2); |
| EXPECT_FALSE(callback.IsReady()); |
| } |
| |
| // --- Throughput scaling --- |
| |
| TEST_F(DelayedStreamSocketTest, LargerReadTakesLonger) { |
| // 100 bytes at 1000 bytes/sec = 100ms throughput delay via shared throttle. |
| const std::string kData(100, 'x'); |
| MockRead reads[] = { |
| MockRead(SYNCHRONOUS, kData), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| // Throughput via shared throttle. Burst = 1000 * 10ms = 10 bytes, so |
| // 100 bytes exceeds burst and requires token accumulation. |
| auto download_throttle = |
| base::MakeRefCounted<BandwidthThrottle>(1000, base::Milliseconds(10)); |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(base::TimeDelta(), |
| /*download_bytes_per_sec=*/1000, |
| /*upload_bytes_per_sec=*/std::nullopt), |
| download_throttle, nullptr); |
| |
| base::test::TestFuture<int> connect_cb; |
| EXPECT_THAT(socket->Connect(connect_cb.GetCallback()), test::IsOk()); |
| |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), 1024, read_cb.GetCallback()), |
| test::IsError(ERR_IO_PENDING)); |
| |
| // 100 bytes at 1000 B/s, burst = 10 bytes. Immediate: 10 bytes consumed. |
| // Remaining 90 bytes need 90ms of token accumulation. |
| FastForwardBy(base::Milliseconds(89)); |
| EXPECT_FALSE(read_cb.IsReady()); |
| |
| FastForwardBy(base::Milliseconds(1)); |
| EXPECT_TRUE(read_cb.IsReady()); |
| EXPECT_EQ(read_cb.Get(), 100); |
| } |
| |
| // --- Async inner socket --- |
| |
| TEST_F(DelayedStreamSocketTest, AsyncReadWithLatency) { |
| const std::string kData = "async hello"; |
| MockRead reads[] = { |
| MockRead(ASYNC, kData), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| // Use unlimited throughput so the buffer only applies latency. |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/std::nullopt)); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), test::IsOk()); |
| |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), 1024, read_cb.GetCallback()), |
| test::IsError(ERR_IO_PENDING)); |
| |
| // Inner async read completes first, then half-RTT latency delay. |
| FastForwardBy(base::Milliseconds(74)); |
| EXPECT_FALSE(read_cb.IsReady()); |
| |
| FastForwardBy(base::Milliseconds(1)); |
| EXPECT_TRUE(read_cb.IsReady()); |
| EXPECT_EQ(read_cb.Get(), static_cast<int>(kData.size())); |
| } |
| |
| // --- Shared throttle tests --- |
| |
| TEST_F(DelayedStreamSocketTest, SharedThrottleCoordinatesBandwidth) { |
| // Two sockets sharing a download throttle at 1000 bytes/sec. |
| auto download_throttle = |
| base::MakeRefCounted<BandwidthThrottle>(1000, base::Milliseconds(100)); |
| |
| const std::string kData = "hello"; // 5 bytes each |
| |
| // Socket 1. |
| MockRead reads1[] = {MockRead(SYNCHRONOUS, kData)}; |
| StaticSocketDataProvider data1(reads1, base::span<MockWrite>()); |
| data1.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| // No per-socket throughput (unlimited), rely on shared throttle. |
| auto config = MakeConfig(base::TimeDelta(), |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/std::nullopt); |
| auto socket1 = std::make_unique<DelayedStreamSocket>( |
| std::make_unique<MockTCPClientSocket>( |
| AddressList(IPEndPoint(IPAddress::IPv4Localhost(), 80)), nullptr, |
| &data1), |
| config, download_throttle, nullptr); |
| |
| // Socket 2. |
| MockRead reads2[] = {MockRead(SYNCHRONOUS, kData)}; |
| StaticSocketDataProvider data2(reads2, base::span<MockWrite>()); |
| data2.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket2 = std::make_unique<DelayedStreamSocket>( |
| std::make_unique<MockTCPClientSocket>( |
| AddressList(IPEndPoint(IPAddress::IPv4Localhost(), 80)), nullptr, |
| &data2), |
| config, download_throttle, nullptr); |
| |
| // Connect both (no latency). |
| base::test::TestFuture<int> cc1, cc2; |
| EXPECT_THAT(socket1->Connect(cc1.GetCallback()), test::IsOk()); |
| EXPECT_THAT(socket2->Connect(cc2.GetCallback()), test::IsOk()); |
| |
| // Read from both simultaneously. Burst size is 100 bytes, so both 5-byte |
| // reads fit in the burst. With a shared throttle, operations go through the |
| // callback path even when tokens are available immediately. |
| auto buffer1 = base::MakeRefCounted<IOBufferWithSize>(1024); |
| auto buffer2 = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> rc1, rc2; |
| |
| // First read: 5 bytes from burst, served via callback (throttle path). |
| EXPECT_THAT(socket1->Read(buffer1.get(), 1024, rc1.GetCallback()), |
| test::IsError(ERR_IO_PENDING)); |
| FastForwardBy(base::TimeDelta()); |
| EXPECT_TRUE(rc1.IsReady()); |
| EXPECT_EQ(rc1.Get(), 5); |
| |
| // Second read: 5 bytes from burst, also served quickly. |
| EXPECT_THAT(socket2->Read(buffer2.get(), 1024, rc2.GetCallback()), |
| test::IsError(ERR_IO_PENDING)); |
| FastForwardBy(base::TimeDelta()); |
| EXPECT_TRUE(rc2.IsReady()); |
| EXPECT_EQ(rc2.Get(), 5); |
| } |
| |
| TEST_F(DelayedStreamSocketTest, SharedThrottleQueuesWhenExhausted) { |
| // Throttle at 100 bytes/sec, burst = 10 bytes (100ms burst). |
| auto download_throttle = |
| base::MakeRefCounted<BandwidthThrottle>(100, base::Milliseconds(100)); |
| |
| const std::string kData(10, 'x'); // 10 bytes; exhausts burst. |
| |
| MockRead reads1[] = {MockRead(SYNCHRONOUS, kData)}; |
| StaticSocketDataProvider data1(reads1, base::span<MockWrite>()); |
| data1.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| |
| MockRead reads2[] = {MockRead(SYNCHRONOUS, kData)}; |
| StaticSocketDataProvider data2(reads2, base::span<MockWrite>()); |
| data2.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| |
| auto config = MakeConfig(base::TimeDelta(), |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/std::nullopt); |
| auto socket1 = std::make_unique<DelayedStreamSocket>( |
| std::make_unique<MockTCPClientSocket>( |
| AddressList(IPEndPoint(IPAddress::IPv4Localhost(), 80)), nullptr, |
| &data1), |
| config, download_throttle, nullptr); |
| auto socket2 = std::make_unique<DelayedStreamSocket>( |
| std::make_unique<MockTCPClientSocket>( |
| AddressList(IPEndPoint(IPAddress::IPv4Localhost(), 80)), nullptr, |
| &data2), |
| config, download_throttle, nullptr); |
| |
| base::test::TestFuture<int> cc1, cc2; |
| EXPECT_THAT(socket1->Connect(cc1.GetCallback()), test::IsOk()); |
| EXPECT_THAT(socket2->Connect(cc2.GetCallback()), test::IsOk()); |
| |
| // First read: 10 bytes exhausts the burst; throttle serves immediately |
| // but callback is posted async. |
| auto buffer1 = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> rc1; |
| EXPECT_THAT(socket1->Read(buffer1.get(), 1024, rc1.GetCallback()), |
| test::IsError(ERR_IO_PENDING)); |
| FastForwardBy(base::TimeDelta()); |
| EXPECT_EQ(rc1.Get(), 10); |
| |
| // Second read: burst exhausted, must wait. 10 bytes at 100 bps = 100ms. |
| auto buffer2 = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> rc2; |
| EXPECT_THAT(socket2->Read(buffer2.get(), 1024, rc2.GetCallback()), |
| test::IsError(ERR_IO_PENDING)); |
| |
| FastForwardBy(base::Milliseconds(99)); |
| EXPECT_FALSE(rc2.IsReady()); |
| |
| FastForwardBy(base::Milliseconds(1)); |
| EXPECT_TRUE(rc2.IsReady()); |
| EXPECT_EQ(rc2.Get(), 10); |
| } |
| |
| // --- Regression: async inner reads must respect download throttle |
| // backpressure. If MaybeStartInnerRead() proceeded while a shared |
| // download-throttle grant was pending, it would issue an inner Read |
| // behind the throttle's back and read-ahead would run uncapped. |
| |
| TEST_F(DelayedStreamSocketTest, |
| AsyncInnerReadStopsWhileDownloadThrottlePending) { |
| // 100 B/s, burst 10 bytes; a Read that fills the buffer with 100+ |
| // bytes will strand the throttle grant in-flight for ~1s while we |
| // observe that no further inner Read has been issued. |
| auto download_throttle = |
| base::MakeRefCounted<BandwidthThrottle>(100, base::Milliseconds(100)); |
| |
| const std::string kFirst(100, 'a'); |
| const std::string kSecond(100, 'b'); |
| const std::string kThird(100, 'c'); |
| MockRead reads[] = { |
| MockRead(ASYNC, kFirst), |
| MockRead(ASYNC, kSecond), |
| MockRead(ASYNC, kThird), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(base::TimeDelta(), |
| /*download_bytes_per_sec=*/100, |
| /*upload_bytes_per_sec=*/std::nullopt), |
| download_throttle, /*upload_throttle=*/nullptr); |
| |
| base::test::TestFuture<int> connect_cb; |
| EXPECT_THAT(socket->Connect(connect_cb.GetCallback()), test::IsOk()); |
| |
| // First inner Read is issued by StartInnerRead(). Consumer's Read is |
| // pending until throttle admits. |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), 1024, read_cb.GetCallback()), |
| test::IsError(ERR_IO_PENDING)); |
| EXPECT_EQ(data_provider.read_index(), 1u); |
| |
| // Let the first async inner Read fire. OnInnerReadComplete fills the |
| // buffer with 100 bytes and requests a 1000-byte throttle grant; |
| // burst is only 10 bytes so `download_throttle_pending_` stays true. |
| // MaybeStartInnerRead() must skip issuing the next inner Read. |
| FastForwardBy(base::Milliseconds(1)); |
| EXPECT_EQ(data_provider.read_index(), 1u); |
| |
| // While the throttle grant is still pending, read_index must not |
| // advance even after a substantial delay: no read-ahead behind |
| // backpressure. |
| FastForwardBy(base::Milliseconds(50)); |
| EXPECT_EQ(data_provider.read_index(), 1u); |
| |
| // Once the throttle grants (100 B/s at 90ms + initial 10-byte burst), |
| // the consumer receives its bytes and read-ahead resumes. |
| FastForwardBy(base::Seconds(2)); |
| EXPECT_TRUE(read_cb.IsReady()); |
| EXPECT_GT(read_cb.Get(), 0); |
| } |
| |
| // --- Leftover throttle grant is reused across ReadIfReady calls --- |
| |
| TEST_F(DelayedStreamSocketTest, LeftoverGrantReusedAcrossReadIfReadyCalls) { |
| // ReadIfReady requests a grant for the entire front chunk (max_bytes = |
| // INT_MAX). When the caller pulls less than the chunk, the leftover grant |
| // must persist so the next call doesn't re-charge the throttle. With a |
| // slow throttle, the grant should only be requested once and the rest |
| // pulled synchronously from the leftover. |
| auto download_throttle = base::MakeRefCounted<BandwidthThrottle>( |
| /*throughput_bytes_per_sec=*/100, base::Seconds(1)); // burst = 100 bytes |
| |
| const std::string kData(100, 'a'); |
| MockRead reads[] = {MockRead(SYNCHRONOUS, kData)}; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| |
| auto socket = std::make_unique<DelayedStreamSocket>( |
| std::make_unique<MockTCPClientSocket>( |
| AddressList(IPEndPoint(IPAddress::IPv4Localhost(), 80)), nullptr, |
| &data_provider), |
| MakeConfig(kLatency, /*download_bytes_per_sec=*/100, |
| /*upload_bytes_per_sec=*/ |
| std::nullopt), |
| download_throttle, /*upload_throttle=*/nullptr); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), test::IsOk()); |
| |
| // First ReadIfReady pays half-RTT for latency and a throttle round-trip |
| // for the entire 100-byte chunk (within the burst). |
| auto small_buffer = base::MakeRefCounted<IOBufferWithSize>(10); |
| base::test::TestFuture<int> ready_cb; |
| EXPECT_THAT( |
| socket->ReadIfReady(small_buffer.get(), 10, ready_cb.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| FastForwardBy(kLatency); |
| ASSERT_TRUE(ready_cb.IsReady()); |
| EXPECT_THAT(ready_cb.Get(), IsOk()); |
| |
| // Drain the chunk via small synchronous ReadIfReady calls. With leftover- |
| // grant accounting these must succeed instantly; resetting the grant |
| // (the pre-fix behaviour) would re-request the throttle and the burst |
| // is already exhausted, so each call would have to wait 100 ms. |
| int remaining = 100; |
| while (remaining > 0) { |
| int rv = socket->ReadIfReady(small_buffer.get(), 10, base::DoNothing()); |
| ASSERT_GT(rv, 0); |
| ASSERT_LE(rv, 10); |
| remaining -= rv; |
| } |
| EXPECT_EQ(remaining, 0); |
| } |
| |
| // --- ReadIfReady fires callback with OK, not byte count --- |
| |
| TEST_F(DelayedStreamSocketTest, ReadIfReadySignalsOkNotByteCount) { |
| const std::string kData = "hello"; |
| MockRead reads[] = { |
| MockRead(SYNCHRONOUS, kData), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/std::nullopt)); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), test::IsOk()); |
| |
| // First ReadIfReady should return ERR_IO_PENDING (latency not elapsed). |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> ready_cb; |
| int rv = socket->ReadIfReady(buffer.get(), 1024, ready_cb.GetCallback()); |
| EXPECT_THAT(rv, IsError(ERR_IO_PENDING)); |
| |
| // After latency, callback fires with OK (readiness signal, not byte count). |
| FastForwardBy(kLatency / 2); |
| ASSERT_TRUE(ready_cb.IsReady()); |
| EXPECT_THAT(ready_cb.Get(), IsOk()); |
| |
| // Now re-call ReadIfReady to actually pull the data synchronously. |
| rv = socket->ReadIfReady(buffer.get(), 1024, base::DoNothing()); |
| EXPECT_EQ(rv, static_cast<int>(kData.size())); |
| } |
| |
| // --- Push never drops bytes (inner read limited to free space) --- |
| |
| TEST_F(DelayedStreamSocketTest, BufferBackpressurePreventsDataLoss) { |
| // Create a large payload to ensure the read loop respects buffer capacity |
| // and never drops bytes if Push() accepts less than the inner read size. |
| const std::string kLargeData(500, 'x'); |
| MockRead reads[] = { |
| MockRead(SYNCHRONOUS, kLargeData), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), test::IsOk()); |
| |
| // Read: should eventually return all bytes without dropping any. |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), 1024, read_cb.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| |
| // Fast-forward enough time for latency + throughput to deliver all data. |
| // 75ms latency + 500 bytes / 1000 B/s = 575ms. |
| FastForwardBy(base::Milliseconds(600)); |
| ASSERT_TRUE(read_cb.IsReady()); |
| int bytes_read = read_cb.Get(); |
| EXPECT_GT(bytes_read, 0); |
| // The key assertion: no CHECK failure from Push, and we got data. |
| } |
| |
| TEST_F(DelayedStreamSocketTest, LargeWriteSpansChunksAndPartialAccepts) { |
| // Exercise both the multi-chunk drain path (Write larger than the inner |
| // scratch buffer) and the partial-accept retry path within a chunk. |
| // |
| // The pipeline's inner scratch buffer is 32 KiB. We write 40 KiB and have |
| // the mock socket accept the first chunk in two pieces (partial accept), |
| // then accept the remaining 8 KiB in a second chunk. |
| constexpr int kInnerChunk = 32 * 1024; |
| constexpr int kFirstPartial = 25 * 1024; |
| constexpr int kFirstRemainder = kInnerChunk - kFirstPartial; // 7 KiB |
| constexpr int kTotal = 40 * 1024; |
| constexpr int kSecondChunk = kTotal - kInnerChunk; // 8 KiB |
| |
| const std::string kData(kTotal, 'X'); |
| const std::string kExpectedFirstPartial(kFirstPartial, 'X'); |
| const std::string kExpectedFirstRemainder(kFirstRemainder, 'X'); |
| const std::string kExpectedSecondChunk(kSecondChunk, 'X'); |
| |
| MockWrite writes[] = { |
| MockWrite(SYNCHRONOUS, kExpectedFirstPartial), |
| MockWrite(SYNCHRONOUS, kExpectedFirstRemainder), |
| MockWrite(SYNCHRONOUS, kExpectedSecondChunk), |
| }; |
| StaticSocketDataProvider data_provider(base::span<MockRead>(), writes); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/std::nullopt)); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), test::IsOk()); |
| |
| auto buffer = base::MakeRefCounted<StringIOBuffer>(kData); |
| base::test::TestFuture<int> write_cb; |
| EXPECT_THAT(socket->Write(buffer.get(), kTotal, write_cb.GetCallback(), |
| TRAFFIC_ANNOTATION_FOR_TESTS), |
| IsError(ERR_IO_PENDING)); |
| |
| // After half-RTT the upload buffer is ready to drain: pipeline pulls the |
| // first 32 KiB, the mock accepts 25 KiB, the pipeline shifts and re-issues |
| // the remaining 7 KiB, then pulls and writes the final 8 KiB. All three |
| // mock writes consume synchronously and the caller's Write completes. |
| FastForwardBy(kLatency / 2); |
| ASSERT_TRUE(write_cb.IsReady()); |
| EXPECT_EQ(write_cb.Get(), kTotal); |
| EXPECT_TRUE(data_provider.AllWriteDataConsumed()); |
| } |
| |
| // --- Write error from inner socket is propagated --- |
| |
| TEST_F(DelayedStreamSocketTest, InnerWriteErrorPropagates) { |
| MockWrite writes[] = { |
| MockWrite(SYNCHRONOUS, ERR_CONNECTION_RESET), |
| }; |
| StaticSocketDataProvider data_provider(base::span<MockRead>(), writes); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/std::nullopt)); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), test::IsOk()); |
| |
| auto buffer = base::MakeRefCounted<StringIOBuffer>("hello"); |
| base::test::TestFuture<int> write_cb; |
| EXPECT_THAT(socket->Write(buffer.get(), 5, write_cb.GetCallback(), |
| TRAFFIC_ANNOTATION_FOR_TESTS), |
| IsError(ERR_IO_PENDING)); |
| |
| // Error should propagate through the drain path. |
| FastForwardBy(kLatency); |
| ASSERT_TRUE(write_cb.IsReady()); |
| EXPECT_THAT(write_cb.Get(), IsError(ERR_CONNECTION_RESET)); |
| } |
| |
| // --- Throttle callbacks cancelled on Disconnect --- |
| |
| TEST_F(DelayedStreamSocketTest, DisconnectCancelsThrottleCallbacks) { |
| const std::string kData = "hello"; |
| MockRead reads[] = { |
| MockRead(SYNCHRONOUS, kData), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto download_throttle = |
| base::MakeRefCounted<BandwidthThrottle>(100, base::Milliseconds(10)); |
| |
| // Burst = 100 * 10ms = 1 byte. 5 bytes exceeds burst -> queued. |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/std::nullopt), |
| download_throttle); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), test::IsOk()); |
| |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), 1024, read_cb.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| |
| // Disconnect while throttle callback is pending. This should not crash |
| // or fire the callback after the socket is gone. |
| socket->Disconnect(); |
| |
| // Let the throttle timer fire. If the callback wasn't properly cancelled, |
| // this would UAF or crash. |
| FastForwardBy(base::Seconds(10)); |
| // If we get here without crashing, the test passed. |
| } |
| |
| // --- Upload throttle is consulted during normal drain --- |
| |
| TEST_F(DelayedStreamSocketTest, UploadThrottleIsConsulted) { |
| const std::string kData(200, 'y'); // 200 bytes |
| MockWrite writes[] = { |
| MockWrite(SYNCHRONOUS, kData), |
| }; |
| StaticSocketDataProvider data_provider(base::span<MockRead>(), writes); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| |
| // Shared upload throttle at 100 bytes/sec, burst = 10 bytes. |
| auto upload_throttle = |
| base::MakeRefCounted<BandwidthThrottle>(100, base::Milliseconds(100)); |
| |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/std::nullopt), |
| /*download_throttle=*/nullptr, upload_throttle); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), test::IsOk()); |
| |
| auto buffer = base::MakeRefCounted<StringIOBuffer>(kData); |
| base::test::TestFuture<int> write_cb; |
| EXPECT_THAT(socket->Write(buffer.get(), kData.size(), write_cb.GetCallback(), |
| TRAFFIC_ANNOTATION_FOR_TESTS), |
| IsError(ERR_IO_PENDING)); |
| |
| // With 100 B/s throttle and 200 bytes, it should take ~2 seconds |
| // (minus burst) to complete. Certainly not instant. |
| FastForwardBy(base::Milliseconds(100)); |
| EXPECT_FALSE(write_cb.IsReady()); |
| |
| // After enough time for throttle to process all bytes. |
| FastForwardBy(base::Seconds(5)); |
| ASSERT_TRUE(write_cb.IsReady()); |
| EXPECT_EQ(write_cb.Get(), static_cast<int>(kData.size())); |
| } |
| |
| // --- Inner read error / EOF propagation --- |
| |
| TEST_F(DelayedStreamSocketTest, InnerReadErrorPropagates) { |
| MockRead reads[] = { |
| MockRead(ASYNC, ERR_CONNECTION_RESET), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), 1024, read_cb.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| |
| FastForwardBy(kLatency); |
| ASSERT_TRUE(read_cb.IsReady()); |
| EXPECT_THAT(read_cb.Get(), IsError(ERR_CONNECTION_RESET)); |
| } |
| |
| TEST_F(DelayedStreamSocketTest, InnerReadEofPropagates) { |
| // Empty read (length 0) means EOF in the mock socket. |
| MockRead reads[] = { |
| MockRead(ASYNC, 0), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), 1024, read_cb.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| |
| FastForwardBy(kLatency); |
| ASSERT_TRUE(read_cb.IsReady()); |
| EXPECT_EQ(read_cb.Get(), 0); |
| |
| // Subsequent Read should also see EOF synchronously. |
| EXPECT_EQ(socket->Read(buffer.get(), 1024, base::DoNothing()), 0); |
| } |
| |
| // --- CancelReadIfReady drops the pending callback --- |
| |
| TEST_F(DelayedStreamSocketTest, CancelReadIfReadyDropsPendingCallback) { |
| const std::string kData = "hello"; |
| MockRead reads[] = { |
| MockRead(SYNCHRONOUS, kData), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> ready_cb; |
| EXPECT_THAT(socket->ReadIfReady(buffer.get(), 1024, ready_cb.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| |
| EXPECT_EQ(socket->CancelReadIfReady(), OK); |
| |
| // After latency the data is ready, but the cancelled callback must not fire. |
| FastForwardBy(kLatency); |
| EXPECT_FALSE(ready_cb.IsReady()); |
| |
| // Data should still be buffered and readable via a fresh ReadIfReady. |
| base::test::TestFuture<int> ready_cb2; |
| int rv = socket->ReadIfReady(buffer.get(), 1024, ready_cb2.GetCallback()); |
| if (rv == ERR_IO_PENDING) { |
| ASSERT_TRUE(ready_cb2.Wait()); |
| EXPECT_THAT(ready_cb2.Get(), IsOk()); |
| } else { |
| EXPECT_EQ(rv, static_cast<int>(kData.size())); |
| } |
| } |
| |
| // --- Async inner write path (mock returns ASYNC) --- |
| |
| TEST_F(DelayedStreamSocketTest, AsyncInnerWriteCompletes) { |
| const std::string kData = "hello"; |
| MockWrite writes[] = { |
| MockWrite(ASYNC, kData), |
| }; |
| StaticSocketDataProvider data_provider(base::span<MockRead>(), writes); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| auto buffer = base::MakeRefCounted<StringIOBuffer>(kData); |
| base::test::TestFuture<int> write_cb; |
| EXPECT_THAT(socket->Write(buffer.get(), kData.size(), write_cb.GetCallback(), |
| TRAFFIC_ANNOTATION_FOR_TESTS), |
| IsError(ERR_IO_PENDING)); |
| |
| // Half-RTT for the buffer to become drainable; then the inner write fires |
| // asynchronously and OnInnerWriteComplete propagates completion. |
| FastForwardBy(kLatency); |
| ASSERT_TRUE(write_cb.IsReady()); |
| EXPECT_EQ(write_cb.Get(), static_cast<int>(kData.size())); |
| EXPECT_TRUE(data_provider.AllWriteDataConsumed()); |
| } |
| |
| // --- Reading less than a chunk leaves the rest buffered --- |
| |
| TEST_F(DelayedStreamSocketTest, PartialReadIntoSmallerBuffer) { |
| const std::string kData(100, 'a'); |
| MockRead reads[] = { |
| MockRead(SYNCHRONOUS, kData), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| // First Read with a 10-byte buffer takes half-RTT and returns 10 bytes. |
| auto small_buffer = base::MakeRefCounted<IOBufferWithSize>(10); |
| base::test::TestFuture<int> first_cb; |
| EXPECT_THAT(socket->Read(small_buffer.get(), 10, first_cb.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| FastForwardBy(kLatency / 2); |
| ASSERT_TRUE(first_cb.IsReady()); |
| EXPECT_EQ(first_cb.Get(), 10); |
| |
| // Remaining 90 bytes stay buffered; subsequent Reads complete sync. |
| int remaining = 90; |
| while (remaining > 0) { |
| int rv = socket->Read(small_buffer.get(), 10, base::DoNothing()); |
| ASSERT_GT(rv, 0); |
| ASSERT_LE(rv, 10); |
| remaining -= rv; |
| } |
| EXPECT_EQ(remaining, 0); |
| } |
| |
| // --- Disconnect cancels a pending Write callback --- |
| |
| TEST_F(DelayedStreamSocketTest, DisconnectDuringPendingWrite) { |
| const std::string kData = "hello"; |
| MockWrite writes[] = { |
| MockWrite(SYNCHRONOUS, kData), |
| }; |
| StaticSocketDataProvider data_provider(base::span<MockRead>(), writes); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| auto buffer = base::MakeRefCounted<StringIOBuffer>(kData); |
| base::test::TestFuture<int> write_cb; |
| EXPECT_THAT(socket->Write(buffer.get(), kData.size(), write_cb.GetCallback(), |
| TRAFFIC_ANNOTATION_FOR_TESTS), |
| IsError(ERR_IO_PENDING)); |
| |
| socket->Disconnect(); |
| |
| // After draining time, the cancelled callback must not fire. |
| FastForwardBy(kLatency * 4); |
| EXPECT_FALSE(write_cb.IsReady()); |
| } |
| |
| // --- Write blocks when upload buffer is full, completes when space frees --- |
| |
| TEST_F(DelayedStreamSocketTest, WriteBufferFullThenSpaceFrees) { |
| // Force the smallest possible upload buffer (kMinCapacity = 16 KiB) via a |
| // very low configured throughput (used only for BDP sizing). The shared |
| // throttle uses a generous burst so the drain itself doesn't stall; the |
| // intent here is to exercise the buffer-full backpressure path, not the |
| // throttle's rate limiting. |
| constexpr int kCap = 16 * 1024; // BottleneckBuffer::kMinCapacity |
| constexpr int kFirst = kCap + 4 * 1024; // 20 KiB - partial accept. |
| constexpr int kSecond = kFirst - kCap; // 4 KiB - retried. |
| |
| const std::string kFirstData(kFirst, 'q'); |
| const std::string kSecondData(kSecond, 'q'); |
| const std::string kFirstChunkData(kCap, 'q'); |
| MockWrite writes[] = { |
| // First inner Write of 16 KiB matches the first 16 KiB of this mock. |
| MockWrite(SYNCHRONOUS, kFirstChunkData), |
| // Second inner Write of 4 KiB from the retried push. |
| MockWrite(SYNCHRONOUS, kSecondData), |
| }; |
| StaticSocketDataProvider data_provider(base::span<MockRead>(), writes); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| |
| auto upload_throttle = |
| base::MakeRefCounted<BandwidthThrottle>(1024 * 1024, base::Seconds(1)); |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/10000), |
| /*download_throttle=*/nullptr, upload_throttle); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| // First Write: 20 KiB. Push fills the 16 KiB buffer; caller sees 16 KiB |
| // accepted synchronously (partial accept, not IO_PENDING). |
| auto buffer = base::MakeRefCounted<StringIOBuffer>(kFirstData); |
| int first_rv = socket->Write(buffer.get(), kFirst, base::DoNothing(), |
| TRAFFIC_ANNOTATION_FOR_TESTS); |
| ASSERT_EQ(first_rv, kCap); |
| |
| // Second Write of the remainder: buffer is full, so Push returns 0 and |
| // OnUploadSpaceAvailable will retry once drain frees space. |
| auto remainder = base::MakeRefCounted<StringIOBuffer>(kSecondData); |
| base::test::TestFuture<int> retry_cb; |
| EXPECT_THAT(socket->Write(remainder.get(), kSecond, retry_cb.GetCallback(), |
| TRAFFIC_ANNOTATION_FOR_TESTS), |
| IsError(ERR_IO_PENDING)); |
| |
| // Let the buffer drain and the stashed write be retried. |
| FastForwardBy(base::Seconds(2)); |
| ASSERT_TRUE(retry_cb.IsReady()); |
| EXPECT_EQ(retry_cb.Get(), kSecond); |
| EXPECT_TRUE(data_provider.AllWriteDataConsumed()); |
| } |
| |
| // Stash a Write whose length exceeds the buffer's capacity, so that even a |
| // fully-drained-and-refilled buffer cannot accept the full stashed amount in |
| // one re-Push. Exercises the OnUploadSpaceAvailable resume path delivering a |
| // *partial* async accept (Socket::Write contract allows reporting a count |
| // smaller than the submitted length). The caller is then expected to retry |
| // the remainder. |
| // |
| // To force the buffer to be full at the moment Write2 arrives; rather than |
| // drained synchronously by Write1's `MaybeDrainUploadBuffer`; latency keeps |
| // Write1's chunk "not ready" until time advances. |
| TEST_F(DelayedStreamSocketTest, WriteBufferFullThenSpaceFreesPartialAccept) { |
| // BottleneckBuffer::kMinCapacity; the BdpCapacity floor when the |
| // configured throughput is small. |
| constexpr int kCap = 16 * 1024; |
| // Write1 partial-accepts kCap synchronously and leaves a chunk in the |
| // buffer that latency holds "not ready". Write2's length is 2 * kCap so |
| // the buffer can never absorb it in one Push, forcing the partial-accept |
| // branch in OnUploadSpaceAvailable when room appears. |
| constexpr int kFirst = kCap + 4 * 1024; // 20 KiB |
| constexpr int kSecond = 2 * kCap; // 32 KiB stashed |
| constexpr int kPartialAccept = kCap; // Async resume reports kCap. |
| constexpr int kRemainder = kSecond - kPartialAccept; // 16 KiB to retry. |
| |
| const std::string kFirstChunkData(kCap, 'a'); |
| const std::string kSecondData(kSecond, 'b'); |
| const std::string kPartialAcceptData(kPartialAccept, 'b'); |
| const std::string kRemainderData(kRemainder, 'b'); |
| |
| // Three inner writes drain the queue in 16 KiB chunks: Write1's chunk, |
| // Write2's first partial-accept chunk, then Write2's retried remainder. |
| MockWrite writes[] = { |
| MockWrite(SYNCHRONOUS, kFirstChunkData), |
| MockWrite(SYNCHRONOUS, kPartialAcceptData), |
| MockWrite(SYNCHRONOUS, kRemainderData), |
| }; |
| StaticSocketDataProvider data_provider(base::span<MockRead>(), writes); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| |
| // Generous throttle burst so the drain is not throttle-stalled; the test |
| // isolates the buffer's resume-time partial-accept path. |
| auto upload_throttle = |
| base::MakeRefCounted<BandwidthThrottle>(1024 * 1024, base::Seconds(1)); |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/10000), |
| /*download_throttle=*/nullptr, upload_throttle); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| // Write1: 20 KiB. Push accepts kCap (= buffer capacity), returns kCap sync |
| // (partial-accept). The chunk is in the buffer but not yet ready to drain |
| // because half-RTT has not elapsed. |
| auto first = base::MakeRefCounted<StringIOBuffer>(std::string(kFirst, 'a')); |
| ASSERT_EQ(socket->Write(first.get(), kFirst, base::DoNothing(), |
| TRAFFIC_ANNOTATION_FOR_TESTS), |
| kCap); |
| |
| // Write2: 32 KiB. Buffer is still full (latency holds the chunk), Push |
| // returns 0, and the wrapper stashes the entire IOBuffer for the |
| // OnUploadSpaceAvailable resume path. |
| auto second = base::MakeRefCounted<StringIOBuffer>(kSecondData); |
| base::test::TestFuture<int> resume_cb; |
| ASSERT_THAT(socket->Write(second.get(), kSecond, resume_cb.GetCallback(), |
| TRAFFIC_ANNOTATION_FOR_TESTS), |
| IsError(ERR_IO_PENDING)); |
| |
| // Advance time: half-RTT elapses for Write1's chunk, the buffer drains it |
| // (full -> not-full), OnUploadSpaceAvailable fires, and the stashed 32 KiB |
| // IOBuffer is re-Push'd against a buffer with kCap of free space; so the |
| // re-Push accepts only kCap, and the caller's pending Write completes with |
| // that partial count. |
| FastForwardBy(base::Seconds(2)); |
| ASSERT_TRUE(resume_cb.IsReady()); |
| EXPECT_EQ(resume_cb.Get(), kPartialAccept); |
| |
| // Caller retries the 16 KiB remainder. The buffer is again full from the |
| // resumed Push, so this Write goes through the buffer-full stash path and |
| // completes asynchronously after the next drain. |
| auto remainder = base::MakeRefCounted<StringIOBuffer>(kRemainderData); |
| base::test::TestFuture<int> tail_cb; |
| int tail_rv = |
| socket->Write(remainder.get(), kRemainder, tail_cb.GetCallback(), |
| TRAFFIC_ANNOTATION_FOR_TESTS); |
| if (tail_rv == ERR_IO_PENDING) { |
| FastForwardBy(base::Seconds(2)); |
| ASSERT_TRUE(tail_cb.IsReady()); |
| EXPECT_EQ(tail_cb.Get(), kRemainder); |
| } else { |
| EXPECT_EQ(tail_rv, kRemainder); |
| } |
| FastForwardBy(base::Seconds(2)); |
| EXPECT_TRUE(data_provider.AllWriteDataConsumed()); |
| } |
| |
| // --- Two ReadIfReady cycles back to back --- |
| |
| TEST_F(DelayedStreamSocketTest, ConsecutiveReadIfReadyCycles) { |
| const std::string kData1 = "hello"; |
| const std::string kData2 = "world"; |
| MockRead reads[] = { |
| MockRead(SYNCHRONOUS, kData1), |
| MockRead(SYNCHRONOUS, kData2), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| // Cycle 1: ReadIfReady pends, fires OK after half-RTT, then sync pull. |
| // Cap the sync pull to the first chunk's length so kStream Pull does not |
| // concatenate the read-ahead second chunk into this cycle's result. |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> ready_cb1; |
| EXPECT_THAT(socket->ReadIfReady(buffer.get(), 1024, ready_cb1.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| FastForwardBy(kLatency / 2); |
| ASSERT_TRUE(ready_cb1.IsReady()); |
| EXPECT_THAT(ready_cb1.Get(), IsOk()); |
| int rv = socket->ReadIfReady(buffer.get(), static_cast<int>(kData1.size()), |
| base::DoNothing()); |
| EXPECT_EQ(rv, static_cast<int>(kData1.size())); |
| |
| // Cycle 2: next chunk was read ahead in parallel during cycle 1, so |
| // ReadIfReady completes synchronously with the byte count. |
| rv = socket->ReadIfReady(buffer.get(), 1024, base::DoNothing()); |
| EXPECT_EQ(rv, static_cast<int>(kData2.size())); |
| } |
| |
| // --- Async inner reads delivered as multiple separate chunks --- |
| |
| TEST_F(DelayedStreamSocketTest, AsyncInnerReadMultipleChunks) { |
| const std::string kData1 = "hello"; |
| const std::string kData2 = "world"; |
| MockRead reads[] = { |
| MockRead(ASYNC, kData1), |
| MockRead(ASYNC, kData2), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| // Cap the first Read to kData1's length so kStream Pull does not |
| // concatenate the read-ahead second chunk into it; the second Read picks |
| // up the second chunk separately. |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb1; |
| EXPECT_THAT(socket->Read(buffer.get(), static_cast<int>(kData1.size()), |
| read_cb1.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| |
| FastForwardBy(kLatency); |
| ASSERT_TRUE(read_cb1.IsReady()); |
| EXPECT_EQ(read_cb1.Get(), static_cast<int>(kData1.size())); |
| |
| base::test::TestFuture<int> read_cb2; |
| int rv = socket->Read(buffer.get(), 1024, read_cb2.GetCallback()); |
| if (rv == ERR_IO_PENDING) { |
| FastForwardBy(kLatency); |
| ASSERT_TRUE(read_cb2.IsReady()); |
| EXPECT_EQ(read_cb2.Get(), static_cast<int>(kData2.size())); |
| } else { |
| EXPECT_EQ(rv, static_cast<int>(kData2.size())); |
| } |
| } |
| |
| // --- Passthrough delegations --- |
| |
| TEST_F(DelayedStreamSocketTest, BufferSizeSettersDelegate) { |
| StaticSocketDataProvider data_provider; |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| // Mock returns OK by default for these setters; just verify they reach |
| // the inner socket (no crash, return propagates). |
| data_provider.set_set_receive_buffer_size_result(OK); |
| data_provider.set_set_send_buffer_size_result(OK); |
| EXPECT_EQ(socket->SetReceiveBufferSize(8192), OK); |
| EXPECT_EQ(socket->SetSendBufferSize(8192), OK); |
| } |
| |
| TEST_F(DelayedStreamSocketTest, SocketDnsAliasesRoundTripThroughDelegate) { |
| // Per-socket DNS aliases (Socket::SetDnsAliases / GetDnsAliases for |
| // CORS / HSTS); unrelated to the DNS resolver delay that lives on the |
| // socket_delay_primitives_dns branch. |
| StaticSocketDataProvider data_provider; |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| std::set<std::string> aliases{"a.example", "b.example"}; |
| socket->SetDnsAliases(aliases); |
| EXPECT_EQ(socket->GetDnsAliases(), aliases); |
| } |
| |
| TEST_F(DelayedStreamSocketTest, AddressAndStateGettersDelegate) { |
| StaticSocketDataProvider data_provider; |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| EXPECT_TRUE(socket->IsConnected()); |
| EXPECT_TRUE(socket->IsConnectedAndIdle()); |
| |
| IPEndPoint peer; |
| EXPECT_EQ(socket->GetPeerAddress(&peer), OK); |
| IPEndPoint local; |
| EXPECT_EQ(socket->GetLocalAddress(&local), OK); |
| |
| // Untouched-state getters do not crash. |
| EXPECT_FALSE(socket->WasEverUsed()); |
| SSLInfo ssl_info; |
| EXPECT_FALSE(socket->GetSSLInfo(&ssl_info)); |
| EXPECT_GE(socket->GetTotalReceivedBytes(), 0); |
| EXPECT_EQ(socket->GetNegotiatedProtocol(), NextProto::kProtoUnknown); |
| // NetLog passthrough must not crash; we just need to touch it. |
| (void)socket->NetLog(); |
| |
| // ApplySocketTag passes through without crashing. |
| socket->ApplySocketTag(SocketTag()); |
| } |
| |
| // --- End-to-end: buffer-full stash resumes via OnUploadSpaceAvailable --- |
| |
| TEST_F(DelayedStreamSocketTest, |
| OnUploadSpaceAvailableResumesStashedWriteWithThrottle) { |
| // With an upload throttle present the throttle grant is always |
| // delivered on a fresh task (see the comment in |
| // BandwidthThrottle::RequestBytes about never re-entering the |
| // caller's Read/Write contract), so this test does NOT exercise a |
| // synchronous drain inside OnUploadSpaceAvailable's frame; that |
| // path is defense-in-depth for the no-upload-throttle configuration |
| // (which requires unlimited upload throughput per the constructor |
| // CHECK and therefore a 2 MiB default buffer). |
| // |
| // What we exercise here is the full round trip: partial-accept |
| // Write1 fills the buffer, Write2 stashes, the throttled drain |
| // empties the buffer, OnUploadSpaceAvailable re-Push()es the stash, |
| // and the caller's completion is posted with the correct byte count. |
| constexpr int kCap = 16 * 1024; |
| const std::string kFirstData(kCap + 4 * 1024, 'q'); |
| const std::string kRetryData(4 * 1024, 'q'); |
| const std::string kFirstChunkData(kCap, 'q'); |
| |
| // Mock accepts the first 16 KiB chunk and the retried 4 KiB chunk. |
| MockWrite writes[] = { |
| MockWrite(SYNCHRONOUS, kFirstChunkData), |
| MockWrite(SYNCHRONOUS, kRetryData), |
| }; |
| StaticSocketDataProvider data(base::span<MockRead>(), writes); |
| data.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| |
| // Very small latency so the upload buffer becomes drainable almost |
| // immediately. No upload throttle so the drain runs entirely |
| // synchronously inside MaybeDrainUploadBuffer. |
| DelayedSocketConfig config{ |
| .rtt = base::Milliseconds(1), |
| .download_throughput_bytes_per_sec = std::nullopt, |
| .upload_throughput_bytes_per_sec = 10000, |
| }; |
| auto socket = std::make_unique<DelayedStreamSocket>( |
| std::make_unique<MockTCPClientSocket>( |
| AddressList(IPEndPoint(IPAddress::IPv4Localhost(), 80)), nullptr, |
| &data), |
| config, /*download_throttle=*/nullptr, |
| base::MakeRefCounted<BandwidthThrottle>(/*throughput_bytes_per_sec=*/ |
| 1024 * 1024, base::Seconds(1))); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(base::Milliseconds(1)); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| // First Write: takes 16 KiB (capacity), leaving 4 KiB to retry later. |
| auto first_buffer = base::MakeRefCounted<StringIOBuffer>(kFirstData); |
| ASSERT_EQ(socket->Write(first_buffer.get(), kFirstData.size(), |
| base::DoNothing(), TRAFFIC_ANNOTATION_FOR_TESTS), |
| kCap); |
| |
| // Second Write: buffer is full; stashed. After the first chunk drains |
| // and space frees, OnUploadSpaceAvailable retries this push. |
| auto retry_buffer = base::MakeRefCounted<StringIOBuffer>(kRetryData); |
| base::test::TestFuture<int> retry_cb; |
| EXPECT_THAT( |
| socket->Write(retry_buffer.get(), kRetryData.size(), |
| retry_cb.GetCallback(), TRAFFIC_ANNOTATION_FOR_TESTS), |
| IsError(ERR_IO_PENDING)); |
| |
| FastForwardBy(base::Seconds(2)); |
| ASSERT_TRUE(retry_cb.IsReady()); |
| EXPECT_EQ(retry_cb.Get(), static_cast<int>(kRetryData.size())); |
| } |
| |
| // --- Regression: Disconnect/Connect cycle rebinds buffer callbacks --- |
| |
| TEST_F(DelayedStreamSocketTest, ReconnectAfterDisconnectDoesNotCrash) { |
| // After Disconnect() invalidates all WeakPtrs the |
| // BottleneckBuffer's data_ready/space_available callbacks would be dead. |
| // Connect() must rebind them. This test just exercises the rebind path; |
| // the mock socket data provider can't easily be reused for a full |
| // round trip, but the absence of a CHECK/UAF on the second Connect() |
| // is itself the meaningful coverage. |
| StaticSocketDataProvider data_provider; |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/std::nullopt)); |
| |
| base::test::TestFuture<int> cc1; |
| socket->Connect(cc1.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(cc1.Get(), IsOk()); |
| socket->Disconnect(); |
| |
| // Second Connect after Disconnect must rebind the buffer wake-up |
| // callbacks and not CHECK / crash. Whatever the mock returns for the |
| // second Connect is fine; we just need the wrapper to survive it. |
| int rv = socket->Connect(base::DoNothing()); |
| EXPECT_NE(rv, ERR_UNEXPECTED); |
| } |
| |
| // --- Regression: CancelReadIfReady clears the pending callback --- |
| |
| // NOTE: The cancelable-closure guard in CancelReadIfReady protects against a |
| // race that is structurally unreachable in MOCK_TIME (every FastForwardBy |
| // atomically drains all zero-delay tasks, so a posted trampoline always |
| // runs before any subsequent test action). This test therefore covers only |
| // the "callback in pending_read_if_ready_callback_, cancel before |
| // CompleteReadIfReady posts" path. The trampoline-already-posted path is |
| // verified by inspection; instrumenting MOCK_TIME to single-step posted |
| // tasks would require a sequence runner harness that isn't worth the |
| // complexity for this regression. |
| TEST_F(DelayedStreamSocketTest, CancelReadIfReadyClearsPendingCallback) { |
| const std::string kData = "hello"; |
| MockRead reads[] = { |
| MockRead(SYNCHRONOUS, kData), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> ready_cb; |
| EXPECT_THAT(socket->ReadIfReady(buffer.get(), 1024, ready_cb.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| |
| // Cancel before half-RTT elapses; the callback is still held in |
| // pending_read_if_ready_callback_ and CompleteReadIfReady has not run. |
| EXPECT_EQ(socket->CancelReadIfReady(), OK); |
| |
| // Drain everything. The cancelled callback must not fire. |
| FastForwardBy(base::Seconds(1)); |
| EXPECT_FALSE(ready_cb.IsReady()); |
| } |
| |
| // --- Regression: zero-byte inner write maps to ERR_CONNECTION_CLOSED --- |
| |
| TEST_F(DelayedStreamSocketTest, ZeroByteInnerWriteTerminatesPipeline) { |
| // An inner socket that returns 0 for a positive-length |
| // write means it's closed; the pipeline must surface ERR_CONNECTION_CLOSED |
| // rather than spinning on the same chunk forever. |
| const std::string kData = "hello"; |
| MockWrite writes[] = { |
| // Mock with empty data + result=0 returns 0 from the inner Write. |
| MockWrite(SYNCHRONOUS, /*result=*/0), |
| }; |
| StaticSocketDataProvider data(base::span<MockRead>(), writes); |
| data.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = |
| CreateSocket(&data, MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/std::nullopt)); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| auto buffer = base::MakeRefCounted<StringIOBuffer>(kData); |
| base::test::TestFuture<int> write_cb; |
| EXPECT_THAT(socket->Write(buffer.get(), kData.size(), write_cb.GetCallback(), |
| TRAFFIC_ANNOTATION_FOR_TESTS), |
| IsError(ERR_IO_PENDING)); |
| FastForwardBy(kLatency); |
| ASSERT_TRUE(write_cb.IsReady()); |
| EXPECT_EQ(write_cb.Get(), ERR_CONNECTION_CLOSED); |
| } |
| |
| // --- Regression: WasEverUsed reports the wrapper's own I/O, not the |
| // inner transport's (StreamSocket contract for layered sockets). |
| TEST_F(DelayedStreamSocketTest, WasEverUsedTracksOwnRead) { |
| const std::string kData = "hi"; |
| MockRead reads[] = {MockRead(SYNCHRONOUS, kData)}; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| // Connect alone (no Read/Write yet) must not flip WasEverUsed even |
| // though the wrapper may have triggered an inner read-ahead on its own. |
| EXPECT_FALSE(socket->WasEverUsed()); |
| |
| // A wrapper-level Read that completes with a positive byte count |
| // flips WasEverUsed. |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| int rv = socket->Read(buffer.get(), 1024, read_cb.GetCallback()); |
| if (rv == ERR_IO_PENDING) { |
| FastForwardBy(kLatency); |
| ASSERT_TRUE(read_cb.IsReady()); |
| rv = read_cb.Get(); |
| } |
| EXPECT_GT(rv, 0); |
| EXPECT_TRUE(socket->WasEverUsed()); |
| } |
| |
| TEST_F(DelayedStreamSocketTest, WasEverUsedTracksOwnWrite) { |
| // Fresh socket (no reads) so we can isolate the Write path's |
| // contribution to WasEverUsed(). |
| const std::string kData = "hi"; |
| MockWrite writes[] = {MockWrite(SYNCHRONOUS, kData)}; |
| StaticSocketDataProvider data_provider(base::span<MockRead>(), writes); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| EXPECT_FALSE(socket->WasEverUsed()); |
| |
| // A wrapper-level Write that completes with a positive byte count |
| // flips WasEverUsed. |
| auto buffer = base::MakeRefCounted<StringIOBuffer>(std::string(kData)); |
| base::test::TestFuture<int> write_cb; |
| int rv = socket->Write(buffer.get(), static_cast<int>(kData.size()), |
| write_cb.GetCallback(), TRAFFIC_ANNOTATION_FOR_TESTS); |
| if (rv == ERR_IO_PENDING) { |
| FastForwardBy(kLatency); |
| ASSERT_TRUE(write_cb.IsReady()); |
| rv = write_cb.Get(); |
| } |
| EXPECT_GT(rv, 0); |
| EXPECT_TRUE(socket->WasEverUsed()); |
| } |
| |
| // --- Regression: IsConnectedAndIdle returns false when read-ahead bytes |
| // are buffered in our BottleneckBuffer. |
| TEST_F(DelayedStreamSocketTest, IsConnectedAndIdleFalseWithReadAheadBuffered) { |
| // The wrapper's StartInnerRead loop pulls data from the inner socket as |
| // soon as the inner socket is connected, so the inner-transport's |
| // IsConnectedAndIdle would say true while bytes are sitting in our |
| // download buffer. The wrapper must report not-idle so a socket-pool |
| // reuse doesn't deliver stale bytes to the next transaction. |
| const std::string kData = "hello"; |
| MockRead reads[] = {MockRead(SYNCHRONOUS, kData), |
| MockRead(SYNCHRONOUS, ERR_IO_PENDING)}; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| // Trigger read-ahead so the buffer holds the chunk. |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), static_cast<int>(kData.size()), |
| read_cb.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| // Drain the inner read into the wrapper's buffer but DO NOT yet pull it |
| // into the caller; the half-RTT hasn't elapsed. |
| EXPECT_FALSE(read_cb.IsReady()); |
| EXPECT_FALSE(socket->IsConnectedAndIdle()); |
| |
| // Let the first chunk become readable and complete the consumer's Read, |
| // which empties the buffer; the wrapper should then report idle again |
| // (modulo whatever the inner transport reports). |
| FastForwardBy(kLatency); |
| ASSERT_TRUE(read_cb.IsReady()); |
| EXPECT_EQ(read_cb.Get(), static_cast<int>(kData.size())); |
| } |
| |
| // --- Regression: CancelReadIfReady cancels the in-flight throttle |
| // request so it cannot grant bytes to a no-longer-pending read. |
| TEST_F(DelayedStreamSocketTest, |
| CancelReadIfReadyCancelsPendingThrottleRequest) { |
| // A small burst forces the throttle to queue our request; we then |
| // cancel before the grant timer fires. If the cancellation handle were |
| // not reset in CancelReadIfReady the throttle would still spend tokens |
| // to admit this request later; to the detriment of other live |
| // sockets sharing the link. |
| const std::string kData = "hello"; |
| MockRead reads[] = {MockRead(SYNCHRONOUS, kData)}; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| // throughput = 100 B/s, burst = 1 ms -> burst size = 0 (clamped to >=1); |
| // any request is queued for token refill. |
| auto download_throttle = |
| base::MakeRefCounted<BandwidthThrottle>(100, base::Milliseconds(10)); |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/std::nullopt), |
| download_throttle); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> rir_cb; |
| EXPECT_THAT(socket->ReadIfReady(buffer.get(), 1024, rir_cb.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| |
| // Advance time enough for half-RTT to elapse (so the chunk is ready and a |
| // throttle request is queued) but not enough for the throttle to admit |
| // it (admission needs ~40 ms more to refill the token bucket). |
| FastForwardBy(kLatency / 2); |
| |
| // Cancel before the throttle admits the request. |
| EXPECT_EQ(socket->CancelReadIfReady(), OK); |
| |
| // Advance time well past any conceivable throttle timer. The cancelled |
| // request must not fire the consumer's callback after CancelReadIfReady |
| // returned OK (Socket::CancelReadIfReady contract). |
| FastForwardBy(base::Seconds(10)); |
| EXPECT_FALSE(rir_cb.IsReady()); |
| } |
| |
| // --- Regression: ReadIntoCaller's Pull is capped to |
| // `pending_read_throttle_grant_` so a multi-chunk-ready buffer cannot |
| // bypass the shared throttle by handing the caller more bytes than |
| // the throttle authorized. |
| TEST_F(DelayedStreamSocketTest, ReadRespectsThrottleGrantAcrossReadyChunks) { |
| // Two inner reads of 5 bytes each -> two chunks pushed into |
| // download_buffer_. Both become ready at the same time (half-RTT after |
| // they were pushed back-to-back). Without the cap in ReadIntoCaller, |
| // Pull(span of 10) would concatenate both chunks and the caller would |
| // receive 10 bytes after only paying the throttle for the first chunk. |
| const std::string kData1 = "hello"; |
| const std::string kData2 = "world"; |
| MockRead reads[] = { |
| MockRead(SYNCHRONOUS, kData1), |
| MockRead(SYNCHRONOUS, kData2), |
| MockRead(SYNCHRONOUS, ERR_IO_PENDING), |
| }; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| // Generous bucket so the grant for the first chunk is immediate; the |
| // test is about cap-on-Pull, not throttle pacing. |
| auto download_throttle = |
| base::MakeRefCounted<BandwidthThrottle>(1024 * 1024, base::Seconds(1)); |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/1024 * 1024, |
| /*upload_bytes_per_sec=*/std::nullopt), |
| download_throttle); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| // Request 10 bytes; enough to span both ready chunks. |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), 10, read_cb.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| |
| FastForwardBy(kLatency); |
| ASSERT_TRUE(read_cb.IsReady()); |
| // Only the first chunk's worth of bytes is allowed through on this |
| // grant. The throttle controls bandwidth on the link; the cap ensures |
| // the next chunk waits for its own grant. |
| EXPECT_EQ(read_cb.Get(), static_cast<int>(kData1.size())); |
| |
| // The second chunk is delivered on the next Read, which must request a |
| // fresh throttle grant before pulling. |
| base::test::TestFuture<int> read_cb2; |
| int rv2 = socket->Read(buffer.get(), 10, read_cb2.GetCallback()); |
| if (rv2 == ERR_IO_PENDING) { |
| FastForwardBy(kLatency); |
| ASSERT_TRUE(read_cb2.IsReady()); |
| rv2 = read_cb2.Get(); |
| } |
| EXPECT_EQ(rv2, static_cast<int>(kData2.size())); |
| } |
| |
| // --- Regression: OnUploadSpaceAvailable surfaces a synchronous inner-write |
| // error encountered during the resume drain instead of silently |
| // dropping it. |
| TEST_F(DelayedStreamSocketTest, OnUploadSpaceAvailableSurfacesDrainError) { |
| // Buffer cap = kMinCapacity = 16 KiB. Write1 (20 KiB) partial-accepts |
| // 16 KiB sync and leaves a chunk in the buffer. Write2 (4 KiB) finds |
| // the buffer full and is stashed. When latency elapses and the |
| // upload throttle admits, the resulting drain calls |
| // HandleInnerWriteResult which sees the inner mock's error and must |
| // route it to Write2's pending callback (the error branch fires |
| // pending_write_callback_ regardless of `pending_write_buffer_`). |
| constexpr int kCap = 16 * 1024; |
| const std::string kFirstData(kCap + 4 * 1024, 'a'); |
| const std::string kFirstChunkData(kCap, 'a'); |
| const std::string kSecondData(4 * 1024, 'b'); |
| // First inner Write returns SYNC error; the buffer is Reset and the |
| // pending Write2 callback gets that error. |
| MockWrite writes[] = { |
| MockWrite(SYNCHRONOUS, ERR_CONNECTION_RESET), |
| }; |
| StaticSocketDataProvider data_provider(base::span<MockRead>(), writes); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto upload_throttle = |
| base::MakeRefCounted<BandwidthThrottle>(1024 * 1024, base::Seconds(1)); |
| auto socket = CreateSocket(&data_provider, |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/10000), |
| /*download_throttle=*/nullptr, upload_throttle); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| // Sync partial-accept: 16 KiB taken, 4 KiB left for the caller to retry. |
| auto first = base::MakeRefCounted<StringIOBuffer>(kFirstData); |
| ASSERT_EQ(socket->Write(first.get(), kFirstData.size(), base::DoNothing(), |
| TRAFFIC_ANNOTATION_FOR_TESTS), |
| kCap); |
| |
| // Buffer-full stash for the retry. This callback is the one that must |
| // see the inner-write error once the resume drain runs. |
| auto second = base::MakeRefCounted<StringIOBuffer>(kSecondData); |
| base::test::TestFuture<int> retry_cb; |
| EXPECT_THAT( |
| socket->Write(second.get(), kSecondData.size(), retry_cb.GetCallback(), |
| TRAFFIC_ANNOTATION_FOR_TESTS), |
| IsError(ERR_IO_PENDING)); |
| |
| // Drain timer + throttle grant + synchronous inner Write returning the |
| // error; OnUploadSpaceAvailable resumes the stashed write, the drain |
| // synchronously errors via HandleInnerWriteResult, and that path fires |
| // the pending Write2 callback with the error. |
| FastForwardBy(base::Seconds(2)); |
| ASSERT_TRUE(retry_cb.IsReady()); |
| EXPECT_EQ(retry_cb.Get(), ERR_CONNECTION_RESET); |
| } |
| |
| // --- Regression: was_ever_used_ flips only on positive completion. |
| TEST_F(DelayedStreamSocketTest, WasEverUsedFalseWhilePendingRead) { |
| // Read that immediately pends (no bytes ready, inner Read async) must |
| // not flip WasEverUsed(). Only positive completions count, matching |
| // TCPClientSocket. |
| MockRead reads[] = {MockRead(ASYNC, ERR_IO_PENDING)}; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), 1024, read_cb.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| // No positive completion happened; the wrapper must not report used. |
| EXPECT_FALSE(socket->WasEverUsed()); |
| } |
| |
| // --- Regression: WasEverUsed resets on reconnect. |
| TEST_F(DelayedStreamSocketTest, WasEverUsedResetsOnReconnect) { |
| const std::string kData = "hi"; |
| MockRead reads[] = {MockRead(SYNCHRONOUS, kData)}; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| // Read that succeeds -> WasEverUsed becomes true. |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| int rv = socket->Read(buffer.get(), 1024, read_cb.GetCallback()); |
| if (rv == ERR_IO_PENDING) { |
| FastForwardBy(kLatency); |
| rv = read_cb.Get(); |
| } |
| ASSERT_GT(rv, 0); |
| EXPECT_TRUE(socket->WasEverUsed()); |
| |
| // Disconnect -> Connect resets the flag (matches TCPClientSocket's |
| // previously_disconnected_ handling). |
| socket->Disconnect(); |
| int reconnect_rv = socket->Connect(base::DoNothing()); |
| // Second Connect on the mock returns whatever it returns; we only care |
| // that the flag has been reset. |
| EXPECT_NE(reconnect_rv, ERR_UNEXPECTED); |
| EXPECT_FALSE(socket->WasEverUsed()); |
| } |
| |
| // --- Regression: IsConnected respects our own connect timer. |
| TEST_F(DelayedStreamSocketTest, IsConnectedFalseWhileConnectPending) { |
| // Wrapped socket completes Connect synchronously, but our latency |
| // timer holds the caller's completion for one RTT. During that time |
| // IsConnected() must report false so callers respect the wrapper's |
| // state machine (not the inner socket's). |
| StaticSocketDataProvider data_provider; |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| EXPECT_FALSE(connect_cb.IsReady()); |
| EXPECT_FALSE(socket->IsConnected()); |
| EXPECT_FALSE(socket->IsConnectedAndIdle()); |
| |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| EXPECT_TRUE(socket->IsConnected()); |
| } |
| |
| TEST_F(DelayedStreamSocketTest, |
| IsConnectedFalseWhileWrappedAsyncConnectPending) { |
| // Wrapped Connect returns ERR_IO_PENDING; before the wrapped socket |
| // fires its own completion (which would then start our latency |
| // timer), our IsConnected() must still report false. This covers the |
| // narrow window between Connect() entry and the timer being armed: |
| // the earlier `connect_timer_.IsRunning()`-only guard missed this |
| // window, so `connect_pending_` widens it. |
| StaticSocketDataProvider data_provider; |
| data_provider.set_connect_data(MockConnect(ASYNC, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| int rv = socket->Connect(connect_cb.GetCallback()); |
| EXPECT_THAT(rv, IsError(ERR_IO_PENDING)); |
| // Wrapped is still async-pending; our own state machine is pending too. |
| EXPECT_FALSE(socket->IsConnected()); |
| EXPECT_FALSE(socket->IsConnectedAndIdle()); |
| |
| // Full RTT (wrapped completes on next task hop, then our timer runs |
| // for kLatency). |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| EXPECT_TRUE(socket->IsConnected()); |
| } |
| |
| // --- Regression: IsConnected reports connected while buffered data remains |
| // even after the inner socket closes. |
| TEST_F(DelayedStreamSocketTest, |
| IsConnectedTrueWithBufferedDataAfterInnerClose) { |
| // Inner delivers one packet then closes with EOF. After the wrapper |
| // has read-ahead the packet and the inner is closed, IsConnected must |
| // still return true until the consumer drains the buffered bytes |
| // (per StreamSocket::IsConnected: "True is returned if the connection |
| // was terminated, but there is unread data in the incoming buffer."). |
| const std::string kData = "hello"; |
| MockRead reads[] = {MockRead(SYNCHRONOUS, kData), |
| MockRead(SYNCHRONOUS, /*result=*/0)}; // EOF |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| // Trigger read-ahead so the wrapper drains the packet and sees EOF. |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), static_cast<int>(kData.size()), |
| read_cb.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| |
| // Before the half-RTT elapses: buffer has bytes, inner may or may not |
| // have signalled EOF depending on mock scheduling. Either way, |
| // IsConnected must report true because there's data to deliver. |
| EXPECT_TRUE(socket->IsConnected()); |
| |
| // Drain to completion. |
| FastForwardBy(kLatency); |
| EXPECT_EQ(read_cb.Get(), static_cast<int>(kData.size())); |
| } |
| |
| // --- Regression: Read on a disconnected socket fails synchronously. |
| TEST_F(DelayedStreamSocketTest, |
| ReadOnDisconnectedSocketReturnsSocketNotConnected) { |
| // If the caller invokes Read before Connect (or after the inner has |
| // gone away with no buffered data), the wrapper should synchronously |
| // return ERR_SOCKET_NOT_CONNECTED instead of pending + async error. |
| StaticSocketDataProvider data_provider; |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| // Deliberately do NOT call Connect. |
| |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| EXPECT_THAT(socket->Read(buffer.get(), 1024, base::DoNothing()), |
| IsError(ERR_SOCKET_NOT_CONNECTED)); |
| } |
| |
| TEST_F(DelayedStreamSocketTest, |
| WriteOnDisconnectedSocketReturnsSocketNotConnected) { |
| // Symmetric to the Read case: Write must synchronously fail with |
| // ERR_SOCKET_NOT_CONNECTED on an unconnected wrapper, not queue bytes |
| // that will never leave. |
| StaticSocketDataProvider data_provider; |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| // Deliberately do NOT call Connect. |
| |
| auto buffer = base::MakeRefCounted<StringIOBuffer>("hi"); |
| EXPECT_THAT(socket->Write(buffer.get(), 2, base::DoNothing(), |
| TRAFFIC_ANNOTATION_FOR_TESTS), |
| IsError(ERR_SOCKET_NOT_CONNECTED)); |
| } |
| |
| // --- Regression: was_ever_used_ flips via DispatchPendingCompletion for |
| // async positive completions on the shaped Read path. |
| TEST_F(DelayedStreamSocketTest, WasEverUsedTrueOnShapedAsyncCompletion) { |
| // Force the async completion path: shaped Read (with latency), inner |
| // Read returns ASYNC. The consumer's callback fires via |
| // DispatchPendingCompletion after half-RTT elapses. That path must |
| // flip WasEverUsed to true (not just the sync fast-path). |
| MockRead reads[] = {MockRead(ASYNC, "hello")}; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| EXPECT_FALSE(socket->WasEverUsed()); |
| |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), 1024, read_cb.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| // Async inner Read + half-RTT latency: completion is scheduled through |
| // DispatchPendingCompletion. |
| FastForwardBy(kLatency); |
| ASSERT_TRUE(read_cb.IsReady()); |
| EXPECT_GT(read_cb.Get(), 0); |
| EXPECT_TRUE(socket->WasEverUsed()); |
| } |
| |
| // --- Regression: was_ever_used_ flips via DidCompletePassthroughIO for |
| // async positive completions on the passthrough Read path. |
| TEST_F(DelayedStreamSocketTest, WasEverUsedTrueOnPassthroughAsyncCompletion) { |
| // Force the passthrough async path: NoDelayConfig (no latency, no |
| // throttle) + ASYNC MockRead. The consumer's callback fires via the |
| // DidCompletePassthroughIO trampoline. That path must flip |
| // WasEverUsed to true. |
| MockRead reads[] = {MockRead(ASYNC, "hello")}; |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, NoDelayConfig()); |
| |
| ASSERT_THAT(socket->Connect(base::DoNothing()), IsOk()); |
| EXPECT_FALSE(socket->WasEverUsed()); |
| |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), 1024, read_cb.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| // Inner Read completes on the next task hop; the passthrough |
| // trampoline forwards through DidCompletePassthroughIO. |
| FastForwardBy(base::TimeDelta()); |
| ASSERT_TRUE(read_cb.IsReady()); |
| EXPECT_GT(read_cb.Get(), 0); |
| EXPECT_TRUE(socket->WasEverUsed()); |
| } |
| |
| // --- Regression: buffered read-ahead survives even after the inner |
| // socket has EOFed; the ERR_SOCKET_NOT_CONNECTED guard must not |
| // interfere with drain. |
| TEST_F(DelayedStreamSocketTest, ReadDrainsBufferedBytesAfterInnerEof) { |
| // Deliver one packet then EOF from the inner socket. After read-ahead |
| // has pulled the packet and observed EOF, the consumer's Read must |
| // still return the buffered bytes (not ERR_SOCKET_NOT_CONNECTED, |
| // which the check would return if it didn't gate on |
| // `download_buffer_.empty()` and `!inner_read_eof_`). |
| const std::string kData = "hello"; |
| MockRead reads[] = {MockRead(SYNCHRONOUS, kData), |
| MockRead(SYNCHRONOUS, /*result=*/0)}; // EOF |
| StaticSocketDataProvider data_provider(reads, base::span<MockWrite>()); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto socket = CreateSocket(&data_provider, MakeConfig()); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| // First Read pends waiting for half-RTT; then delivers the data. |
| auto buffer = base::MakeRefCounted<IOBufferWithSize>(1024); |
| base::test::TestFuture<int> read_cb; |
| EXPECT_THAT(socket->Read(buffer.get(), static_cast<int>(kData.size()), |
| read_cb.GetCallback()), |
| IsError(ERR_IO_PENDING)); |
| FastForwardBy(kLatency); |
| EXPECT_EQ(read_cb.Get(), static_cast<int>(kData.size())); |
| |
| // At this point inner has EOFed (read #2 returned 0). A subsequent |
| // Read must return 0 (EOF), not ERR_SOCKET_NOT_CONNECTED. |
| base::test::TestFuture<int> read_cb2; |
| int rv2 = socket->Read(buffer.get(), 1024, read_cb2.GetCallback()); |
| if (rv2 == ERR_IO_PENDING) { |
| FastForwardBy(kLatency); |
| ASSERT_TRUE(read_cb2.IsReady()); |
| rv2 = read_cb2.Get(); |
| } |
| EXPECT_EQ(rv2, 0); |
| } |
| |
| // --- Regression: sync-partial-accept Write followed by an inner-write |
| // error surfaces the failure on the next Write attempt. |
| TEST_F(DelayedStreamSocketTest, |
| PartialSyncWriteWithInnerErrorSurfacesOnNextWrite) { |
| // The consumer saw a positive sync return from Write, so no callback |
| // fires on the inner-write error. The failure must still be visible |
| // on the next Write. |
| // |
| // Setup: force a small buffer via kMinCapacity (16 KiB) and issue a |
| // Write larger than that so Push returns a partial-accept sync count. |
| // Then have the inner Write fail with ERR_CONNECTION_RESET. The |
| // consumer's second Write must surface an error rather than silently |
| // enqueue more bytes that will never leave. |
| constexpr int kCap = 16 * 1024; // BottleneckBuffer::kMinCapacity |
| constexpr int kFirst = kCap + 4 * 1024; // 20 KiB (partial-accept) |
| // Two failing inner Writes: one for the sync-partial drain (whose |
| // error is silently dropped because no pending callback exists) and |
| // one for the consumer's second Write attempt (whose error must reach |
| // the consumer's callback). |
| MockWrite writes[] = { |
| MockWrite(SYNCHRONOUS, ERR_CONNECTION_RESET), |
| MockWrite(SYNCHRONOUS, ERR_CONNECTION_RESET), |
| }; |
| StaticSocketDataProvider data_provider(base::span<MockRead>(), writes); |
| data_provider.set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| auto upload_throttle = |
| base::MakeRefCounted<BandwidthThrottle>(1024 * 1024, base::Seconds(1)); |
| auto socket = std::make_unique<DelayedStreamSocket>( |
| std::make_unique<MockTCPClientSocket>( |
| AddressList(IPEndPoint(IPAddress::IPv4Localhost(), 80)), nullptr, |
| &data_provider), |
| MakeConfig(kLatency, |
| /*download_bytes_per_sec=*/std::nullopt, |
| /*upload_bytes_per_sec=*/10000), |
| /*download_throttle=*/nullptr, upload_throttle); |
| |
| base::test::TestFuture<int> connect_cb; |
| socket->Connect(connect_cb.GetCallback()); |
| FastForwardBy(kLatency); |
| ASSERT_THAT(connect_cb.Get(), IsOk()); |
| |
| // First Write: sync partial-accept returns `kCap`. No pending callback |
| // is set because bytes were returned sync. |
| auto first = base::MakeRefCounted<StringIOBuffer>(std::string(kFirst, 'a')); |
| ASSERT_EQ(socket->Write(first.get(), kFirst, base::DoNothing(), |
| TRAFFIC_ANNOTATION_FOR_TESTS), |
| kCap); |
| |
| // Let the drain fail. Because there is no pending callback for the |
| // sync-partial bytes, the error is silently dropped inside the |
| // wrapper (upload_buffer_ is Reset). |
| FastForwardBy(base::Seconds(2)); |
| |
| // Consumer's next Write must surface the failure. Either the |
| // wrapped socket is now not-connected (ERR_SOCKET_NOT_CONNECTED sync) |
| // or a fresh async attempt will fire the callback with an error. |
| auto second = base::MakeRefCounted<StringIOBuffer>("tail"); |
| base::test::TestFuture<int> write_cb; |
| int rv = socket->Write(second.get(), 4, write_cb.GetCallback(), |
| TRAFFIC_ANNOTATION_FOR_TESTS); |
| if (rv == ERR_IO_PENDING) { |
| FastForwardBy(base::Seconds(2)); |
| ASSERT_TRUE(write_cb.IsReady()); |
| rv = write_cb.Get(); |
| } |
| EXPECT_LT(rv, 0); |
| } |
| |
| } // namespace |
| } // namespace net |