Updates: when an answer is the rest of the same file, and how long to try

For carrying on a download that broke (issue #54): the head's ETag and
Content-Range are read, a 206 is checked to be the rest of the same
file, and a policy says how many tries a download is worth. Host-tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
2026-10-11 18:26:39 +02:00
co-authored by Claude Opus 5.5
parent 7e14e270df
commit 468dc0dc0d
4 changed files with 159 additions and 0 deletions
+92
View File
@@ -3,6 +3,7 @@
#include <string>
#include "http_head.h"
#include "resume.h"
using namespace roro::release;
@@ -42,6 +43,92 @@ void test_a_download_head() {
TEST_ASSERT_EQUAL_STRING("https://elsewhere.example/x", r.head().location.c_str());
}
void test_a_part_of_a_file() {
std::string head = "HTTP/1.1 206 Partial Content\r\nContent-Range: bytes 1000-2510383/2510384\r\n"
"ETag: \"ac948b3f\"\r\nContent-Length: 2509384\r\n\r\n";
HttpHeadParser p;
p.feed(head.data(), head.size());
TEST_ASSERT_EQUAL_INT(206, p.head().status);
TEST_ASSERT_EQUAL_INT(1000, p.head().rangeFrom);
TEST_ASSERT_EQUAL_INT(2510383, p.head().rangeTo);
TEST_ASSERT_EQUAL_INT(2510384, p.head().rangeTotal);
TEST_ASSERT_EQUAL_STRING("\"ac948b3f\"", p.head().etag.c_str());
// A range that makes no sense is no range.
for (const char* bad : {"bytes */2510384", "bytes 5-2/10", "bytes 0-10/10", "items 0-1/2", ""}) {
std::string h = std::string("HTTP/1.1 206 Partial Content\r\nContent-Range: ") + bad + "\r\n\r\n";
HttpHeadParser q;
q.feed(h.data(), h.size());
TEST_ASSERT_EQUAL_INT(-1, q.head().rangeFrom);
}
}
static HttpHead part(long from, long total, const char* etag = "\"abc\"") {
HttpHead h;
h.status = 206;
h.rangeFrom = from;
h.rangeTo = total - 1;
h.rangeTotal = total;
h.contentLength = total - from;
h.etag = etag;
return h;
}
void test_the_rest_of_the_same_file_is_taken() {
TEST_ASSERT_EQUAL_STRING("", resumeRefusal(part(1000, 5000), 1000, 5000, "\"abc\"").c_str());
TEST_ASSERT_EQUAL_STRING("", resumeRefusal(part(1000, 5000, ""), 1000, 5000, "\"abc\"").c_str()); // no ETag this time
TEST_ASSERT_EQUAL_STRING("", resumeRefusal(part(1000, 5000), 1000, 5000, "").c_str()); // none the first time
}
void test_anything_else_is_refused() {
HttpHead whole;
whole.status = 200; // If-Range did not match: the file is another one now
whole.contentLength = 5000;
TEST_ASSERT_EQUAL_STRING("The file changed on the server", resumeRefusal(whole, 1000, 5000, "\"abc\"").c_str());
HttpHead gone;
gone.status = 404;
TEST_ASSERT_EQUAL_STRING("The server answered 404", resumeRefusal(gone, 1000, 5000, "").c_str());
TEST_ASSERT_FALSE(resumeRefusal(part(0, 5000), 1000, 5000, "\"abc\"").empty()); // from the start
TEST_ASSERT_FALSE(resumeRefusal(part(1000, 6000), 1000, 5000, "\"abc\"").empty()); // another size
TEST_ASSERT_FALSE(resumeRefusal(part(1000, 5000, "\"xyz\""), 1000, 5000, "\"abc\"").empty());
HttpHead shortPart = part(1000, 5000);
shortPart.rangeTo = 2000; // not up to the end
TEST_ASSERT_FALSE(resumeRefusal(shortPart, 1000, 5000, "\"abc\"").empty());
HttpHead chunked = part(1000, 5000);
chunked.chunked = true;
TEST_ASSERT_FALSE(resumeRefusal(chunked, 1000, 5000, "\"abc\"").empty());
}
void test_only_a_strong_etag_goes_in_if_range() {
TEST_ASSERT_EQUAL_STRING("\"abc\"", ifRangeFor("\"abc\"").c_str());
TEST_ASSERT_EQUAL_STRING("", ifRangeFor("W/\"abc\"").c_str());
TEST_ASSERT_EQUAL_STRING("", ifRangeFor("").c_str());
}
void test_how_long_a_download_is_tried() {
ResumePolicy poor; // a poor link: each try brings some more
for (int i = 0; i < ResumePolicy::kMaxResumes; i++) TEST_ASSERT_TRUE(poor.again(1000 * (i + 1)));
TEST_ASSERT_FALSE(poor.again(1000000));
TEST_ASSERT_EQUAL_INT(ResumePolicy::kMaxResumes, poor.resumes());
ResumePolicy dead; // the link is gone: nothing more comes
TEST_ASSERT_TRUE(dead.again(5000));
TEST_ASSERT_TRUE(dead.again(5000));
TEST_ASSERT_TRUE(dead.again(5000));
TEST_ASSERT_FALSE(dead.again(5000));
ResumePolicy fromNothing; // not even a first byte
TEST_ASSERT_TRUE(fromNothing.again(0));
TEST_ASSERT_TRUE(fromNothing.again(0));
TEST_ASSERT_FALSE(fromNothing.again(0));
ResumePolicy recovering; // stuck twice, then bytes again: the count starts over
recovering.again(5000);
recovering.again(5000);
recovering.again(5000);
TEST_ASSERT_TRUE(recovering.again(9000));
TEST_ASSERT_TRUE(recovering.again(9000));
}
void test_a_head_that_is_not_http() {
HttpHeadParser p;
std::string junk = "\x16\x03\x01 not http at all\r\n\r\n";
@@ -114,6 +201,11 @@ int main() {
UNITY_BEGIN();
RUN_TEST(test_a_head_in_pieces);
RUN_TEST(test_a_download_head);
RUN_TEST(test_a_part_of_a_file);
RUN_TEST(test_the_rest_of_the_same_file_is_taken);
RUN_TEST(test_anything_else_is_refused);
RUN_TEST(test_only_a_strong_etag_goes_in_if_range);
RUN_TEST(test_how_long_a_download_is_tried);
RUN_TEST(test_a_head_that_is_not_http);
RUN_TEST(test_chunked_bodies);
RUN_TEST(test_urls);