Skip to content

Commit 8e68909

Browse files
committed
test(download): cover parallel downloads; migrate endpoints to httpbingo.org
New ParallelDownloadTest suite (7 cases) against httpbingo.org, which honors Range on /range/N and serves deterministic content: - segmented result byte-identical to sequential download - maxSegments decoupled from connection count - monotonic parallel progress - fallback when the server ignores Range (200) - small-file fallback to the sequential path - redirect resolution before segmenting - cancellation aborts all workers Also migrate the existing httpbin.org tests to httpbingo.org (same API, more reliable); httpbin.org frequently serves 503s.
1 parent f302873 commit 8e68909

2 files changed

Lines changed: 190 additions & 9 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ build/
22
.xmake/
33
# mcpp build artefacts
44
target/
5+
compile_commands.json

tests/test_download.cpp

Lines changed: 189 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ TEST(DownloadResultContract, CarriesTransferAndResponseMetadata) {
3737
}
3838

3939
// Test download_to_file against a real HTTPS endpoint.
40-
// Uses httpbin.org which returns known-size responses.
40+
// Uses httpbingo.org (a maintained httpbin work-alike) which returns known-size responses.
4141

4242
class DownloadToFileTest : public ::testing::Test {
4343
protected:
@@ -67,7 +67,7 @@ TEST_F(DownloadToFileTest, BasicDownloadWithProgress) {
6767
int callCount = 0;
6868

6969
auto result = client.download_to_file(
70-
"https://httpbin.org/bytes/100",
70+
"https://httpbingo.org/bytes/100",
7171
dest,
7272
[&](std::int64_t total, std::int64_t downloaded) {
7373
lastTotal = total;
@@ -97,7 +97,7 @@ TEST_F(DownloadToFileTest, ProgressIncrementsMonotonically) {
9797
std::vector<std::int64_t> downloadedValues;
9898

9999
auto result = client.download_to_file(
100-
"https://httpbin.org/bytes/51200",
100+
"https://httpbingo.org/bytes/51200",
101101
dest,
102102
[&](std::int64_t total, std::int64_t downloaded) {
103103
(void)total;
@@ -128,9 +128,9 @@ TEST_F(DownloadToFileTest, FollowsRedirects) {
128128

129129
auto dest = tmpDir / "redirected.bin";
130130

131-
// httpbin /redirect-to redirects to the given URL
131+
// httpbingo /redirect-to redirects to the given URL
132132
auto result = client.download_to_file(
133-
"https://httpbin.org/redirect-to?url=https%3A%2F%2Fhttpbin.org%2Fbytes%2F50",
133+
"https://httpbingo.org/redirect-to?url=https%3A%2F%2Fhttpbingo.org%2Fbytes%2F50",
134134
dest
135135
);
136136

@@ -149,7 +149,7 @@ TEST_F(DownloadToFileTest, NoProgressCallbackStillWorks) {
149149
auto dest = tmpDir / "no_progress.bin";
150150

151151
auto result = client.download_to_file(
152-
"https://httpbin.org/bytes/200",
152+
"https://httpbingo.org/bytes/200",
153153
dest
154154
);
155155

@@ -167,7 +167,7 @@ TEST_F(DownloadToFileTest, Http404ReturnsError) {
167167
auto dest = tmpDir / "not_found.bin";
168168

169169
auto result = client.download_to_file(
170-
"https://httpbin.org/status/404",
170+
"https://httpbingo.org/status/404",
171171
dest
172172
);
173173

@@ -186,14 +186,194 @@ TEST_F(DownloadToFileTest, TotalBytesKnownForContentLength) {
186186
std::int64_t reportedTotal = -1;
187187

188188
auto result = client.download_to_file(
189-
"https://httpbin.org/bytes/1024",
189+
"https://httpbingo.org/bytes/1024",
190190
dest,
191191
[&](std::int64_t total, [[maybe_unused]] std::int64_t downloaded) {
192192
if (reportedTotal < 0) reportedTotal = total;
193193
}
194194
);
195195

196196
ASSERT_TRUE(result.ok()) << "Error: " << result.error;
197-
// httpbin /bytes/N returns Content-Length: N
197+
// httpbingo /bytes/N returns Content-Length: N
198198
EXPECT_EQ(reportedTotal, 1024);
199199
}
200+
201+
// Test download_to_file_parallel against endpoints with known Range behavior.
202+
// httpbingo.org /range/N honors Range (206) with deterministic content;
203+
// /bytes/N ignores Range (200), which exercises the fallback path.
204+
205+
class ParallelDownloadTest : public ::testing::Test {
206+
protected:
207+
std::filesystem::path tmpDir;
208+
209+
void SetUp() override {
210+
https::Socket::platform_init();
211+
tmpDir = std::filesystem::temp_directory_path() / "tinyhttps_parallel_test";
212+
std::filesystem::create_directories(tmpDir);
213+
}
214+
void TearDown() override {
215+
std::error_code ec;
216+
std::filesystem::remove_all(tmpDir, ec);
217+
}
218+
};
219+
220+
TEST_F(ParallelDownloadTest, SegmentedResultMatchesSequential) {
221+
const std::string url = "https://httpbingo.org/range/65536";
222+
223+
https::HttpClient seqClient({});
224+
auto seqDest = tmpDir / "seq.bin";
225+
auto seq = seqClient.download_to_file(url, seqDest);
226+
ASSERT_TRUE(seq.ok()) << "Sequential error: " << seq.error;
227+
228+
https::HttpClientConfig cfg;
229+
cfg.connectTimeoutMs = 15000;
230+
cfg.readTimeoutMs = 30000;
231+
cfg.maxConnectionsPerFile = 4;
232+
cfg.minSegmentBytes = 1024; // small so test files actually split
233+
https::HttpClient parClient(cfg);
234+
235+
auto parDest = tmpDir / "par.bin";
236+
auto par = parClient.download_to_file_parallel(url, parDest);
237+
ASSERT_TRUE(par.ok()) << "Parallel error: " << par.error;
238+
239+
EXPECT_EQ(par.bytesWritten, 65536);
240+
ASSERT_TRUE(par.expectedBytes.has_value());
241+
EXPECT_EQ(*par.expectedBytes, 65536);
242+
EXPECT_EQ(std::filesystem::file_size(parDest), 65536u);
243+
244+
std::ifstream seqFile(seqDest, std::ios::binary);
245+
std::ifstream parFile(parDest, std::ios::binary);
246+
std::string seqContent{ std::istreambuf_iterator<char>(seqFile),
247+
std::istreambuf_iterator<char>() };
248+
std::string parContent{ std::istreambuf_iterator<char>(parFile),
249+
std::istreambuf_iterator<char>() };
250+
EXPECT_EQ(parContent, seqContent)
251+
<< "Segmented download content differs from sequential";
252+
}
253+
254+
TEST_F(ParallelDownloadTest, MoreSegmentsThanConnections) {
255+
// maxSegments decoupled from connection count (aria2 -s): 16 segments
256+
// pulled by only 2 workers.
257+
https::HttpClientConfig cfg;
258+
cfg.connectTimeoutMs = 15000;
259+
cfg.readTimeoutMs = 30000;
260+
cfg.maxConnectionsPerFile = 2;
261+
cfg.maxSegments = 16;
262+
cfg.minSegmentBytes = 1024;
263+
https::HttpClient client(cfg);
264+
265+
auto dest = tmpDir / "decoupled.bin";
266+
auto result = client.download_to_file_parallel(
267+
"https://httpbingo.org/range/32768", dest);
268+
269+
ASSERT_TRUE(result.ok()) << "Error: " << result.error;
270+
EXPECT_EQ(result.bytesWritten, 32768);
271+
EXPECT_EQ(std::filesystem::file_size(dest), 32768u);
272+
}
273+
274+
TEST_F(ParallelDownloadTest, ProgressIsMonotonic) {
275+
https::HttpClientConfig cfg;
276+
cfg.connectTimeoutMs = 15000;
277+
cfg.readTimeoutMs = 30000;
278+
cfg.maxConnectionsPerFile = 4;
279+
cfg.minSegmentBytes = 1024;
280+
https::HttpClient client(cfg);
281+
282+
auto dest = tmpDir / "monotonic.bin";
283+
std::vector<std::int64_t> values;
284+
std::int64_t reportedTotal = -1;
285+
286+
auto result = client.download_to_file_parallel(
287+
"https://httpbingo.org/range/131072",
288+
dest,
289+
[&](std::int64_t total, std::int64_t downloaded) {
290+
reportedTotal = total;
291+
values.push_back(downloaded);
292+
}
293+
);
294+
295+
ASSERT_TRUE(result.ok()) << "Error: " << result.error;
296+
EXPECT_EQ(reportedTotal, 131072);
297+
ASSERT_FALSE(values.empty());
298+
EXPECT_EQ(values.back(), 131072);
299+
for (std::size_t i = 1; i < values.size(); ++i) {
300+
EXPECT_GT(values[i], values[i - 1])
301+
<< "Parallel progress not monotonic at index " << i;
302+
}
303+
}
304+
305+
TEST_F(ParallelDownloadTest, FallsBackWhenServerIgnoresRange) {
306+
// /bytes/N does not honor Range — probe gets 200 and the body is
307+
// streamed out over the single probe connection.
308+
https::HttpClientConfig cfg;
309+
cfg.connectTimeoutMs = 15000;
310+
cfg.readTimeoutMs = 30000;
311+
cfg.maxConnectionsPerFile = 4;
312+
cfg.minSegmentBytes = 1024;
313+
https::HttpClient client(cfg);
314+
315+
auto dest = tmpDir / "fallback.bin";
316+
auto result = client.download_to_file_parallel(
317+
"https://httpbingo.org/bytes/4096", dest);
318+
319+
ASSERT_TRUE(result.ok()) << "Error: " << result.error;
320+
EXPECT_EQ(result.statusCode, 200);
321+
EXPECT_EQ(result.bytesWritten, 4096);
322+
EXPECT_EQ(std::filesystem::file_size(dest), 4096u);
323+
}
324+
325+
TEST_F(ParallelDownloadTest, SmallFileFallsBackToSequential) {
326+
// File smaller than minSegmentBytes — not worth splitting.
327+
https::HttpClientConfig cfg;
328+
cfg.connectTimeoutMs = 15000;
329+
cfg.readTimeoutMs = 30000;
330+
cfg.maxConnectionsPerFile = 4;
331+
cfg.minSegmentBytes = 1 << 20;
332+
https::HttpClient client(cfg);
333+
334+
auto dest = tmpDir / "small.bin";
335+
auto result = client.download_to_file_parallel(
336+
"https://httpbingo.org/range/2048", dest);
337+
338+
ASSERT_TRUE(result.ok()) << "Error: " << result.error;
339+
EXPECT_EQ(result.statusCode, 200);
340+
EXPECT_EQ(result.bytesWritten, 2048);
341+
}
342+
343+
TEST_F(ParallelDownloadTest, FollowsRedirectBeforeSegmenting) {
344+
https::HttpClientConfig cfg;
345+
cfg.connectTimeoutMs = 15000;
346+
cfg.readTimeoutMs = 30000;
347+
cfg.maxConnectionsPerFile = 4;
348+
cfg.minSegmentBytes = 1024;
349+
https::HttpClient client(cfg);
350+
351+
auto dest = tmpDir / "redirected.bin";
352+
auto result = client.download_to_file_parallel(
353+
"https://httpbingo.org/redirect-to?url=https%3A%2F%2Fhttpbingo.org%2Frange%2F8192",
354+
dest);
355+
356+
ASSERT_TRUE(result.ok()) << "Error: " << result.error;
357+
EXPECT_EQ(result.bytesWritten, 8192);
358+
EXPECT_EQ(result.finalUrl, "https://httpbingo.org/range/8192");
359+
}
360+
361+
TEST_F(ParallelDownloadTest, CancellationAbortsWorkers) {
362+
https::HttpClientConfig cfg;
363+
cfg.connectTimeoutMs = 15000;
364+
cfg.readTimeoutMs = 30000;
365+
cfg.maxConnectionsPerFile = 4;
366+
cfg.minSegmentBytes = 1024;
367+
https::HttpClient client(cfg);
368+
369+
auto dest = tmpDir / "cancelled.bin";
370+
auto result = client.download_to_file_parallel(
371+
"https://httpbingo.org/range/524288", // httpbingo /range caps at 512KiB
372+
dest,
373+
nullptr,
374+
[] { return true; } // cancel immediately
375+
);
376+
377+
EXPECT_FALSE(result.ok());
378+
EXPECT_EQ(result.error, "cancelled");
379+
}

0 commit comments

Comments
 (0)