Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:58:55 +01:00
parent 4af19165ec
commit 68073add76
12458 changed files with 12350765 additions and 2 deletions

View file

@ -0,0 +1,613 @@
#include "testing/testing.hpp"
#include "platform/chunks_download_strategy.hpp"
#include "platform/http_request.hpp"
#include "platform/platform.hpp"
#include "coding/file_reader.hpp"
#include "coding/file_writer.hpp"
#include "coding/internal/file_data.hpp"
#include "base/logging.hpp"
#include "base/std_serialization.hpp"
#include <QtCore/QCoreApplication>
#include <functional>
#include <memory>
#include <vector>
#include "defines.hpp"
namespace downloader_test
{
using namespace downloader;
using namespace std::placeholders;
using std::bind, std::string, std::vector;
char constexpr kTestUrl1[] = "http://localhost:34568/unit_tests/1.txt";
char constexpr kTestUrl404[] = "http://localhost:34568/unit_tests/notexisting_unittest";
char constexpr kTestUrlBigFile[] = "http://localhost:34568/unit_tests/47kb.file";
// Should match file size in tools/python/ResponseProvider.py
int constexpr kBigFileSize = 47684;
class DownloadObserver
{
bool m_progressWasCalled;
// Chunked downloads can return one status per chunk (thread).
vector<DownloadStatus> m_statuses;
// Interrupt download after this number of chunks
int m_chunksToFail;
base::ScopedLogLevelChanger const m_debugLogLevel;
public:
DownloadObserver() : m_chunksToFail(-1), m_debugLogLevel(LDEBUG) { Reset(); }
void CancelDownloadOnGivenChunk(int chunksToFail) { m_chunksToFail = chunksToFail; }
void Reset()
{
m_progressWasCalled = false;
m_statuses.clear();
}
void TestOk()
{
TEST_NOT_EQUAL(0, m_statuses.size(), ("Observer was not called."));
TEST(m_progressWasCalled, ("Download progress wasn't called"));
for (auto const & status : m_statuses)
TEST_EQUAL(status, DownloadStatus::Completed, ());
}
void TestFailed()
{
TEST_NOT_EQUAL(0, m_statuses.size(), ("Observer was not called."));
for (auto const & status : m_statuses)
TEST_EQUAL(status, DownloadStatus::Failed, ());
}
void TestFileNotFound()
{
TEST_NOT_EQUAL(0, m_statuses.size(), ("Observer was not called."));
for (auto const & status : m_statuses)
TEST_EQUAL(status, DownloadStatus::FileNotFound, ());
}
void OnDownloadProgress(HttpRequest & request)
{
m_progressWasCalled = true;
TEST_EQUAL(request.GetStatus(), DownloadStatus::InProgress, ());
// Cancel download if needed
if (m_chunksToFail != -1)
{
--m_chunksToFail;
if (m_chunksToFail == 0)
{
m_chunksToFail = -1;
LOG(LINFO, ("Download canceled"));
QCoreApplication::quit();
}
}
}
virtual void OnDownloadFinish(HttpRequest & request)
{
auto const status = request.GetStatus();
m_statuses.emplace_back(status);
TEST(status != DownloadStatus::InProgress, ());
QCoreApplication::quit();
}
};
struct CancelDownload
{
void OnProgress(HttpRequest & request)
{
TEST_GREATER(request.GetData().size(), 0, ());
delete &request;
QCoreApplication::quit();
}
void OnFinish(HttpRequest &) { TEST(false, ("Should be never called")); }
};
struct DeleteOnFinish
{
void OnProgress(HttpRequest & request) { TEST_GREATER(request.GetData().size(), 0, ()); }
void OnFinish(HttpRequest & request)
{
delete &request;
QCoreApplication::quit();
}
};
UNIT_TEST(DownloaderSimpleGet)
{
DownloadObserver observer;
auto const MakeRequest = [&observer](string const & url)
{
return HttpRequest::Get(url, bind(&DownloadObserver::OnDownloadFinish, &observer, _1),
bind(&DownloadObserver::OnDownloadProgress, &observer, _1));
};
{
// simple success case
std::unique_ptr<HttpRequest> const request{MakeRequest(kTestUrl1)};
// wait until download is finished
QCoreApplication::exec();
observer.TestOk();
TEST_EQUAL(request->GetData(), "Test1", ());
}
observer.Reset();
{
// We DO NOT SUPPORT redirects to avoid data corruption when downloading mwm files
std::unique_ptr<HttpRequest> const request{MakeRequest("http://localhost:34568/unit_tests/permanent")};
QCoreApplication::exec();
observer.TestFailed();
TEST_EQUAL(request->GetData().size(), 0, (request->GetData()));
}
observer.Reset();
{
// fail case 404
std::unique_ptr<HttpRequest> const request{MakeRequest(kTestUrl404)};
QCoreApplication::exec();
observer.TestFileNotFound();
TEST_EQUAL(request->GetData().size(), 0, (request->GetData()));
}
observer.Reset();
{
// fail case not existing host
std::unique_ptr<HttpRequest> const request{MakeRequest("http://not-valid-host123532.ath.cx")};
QCoreApplication::exec();
observer.TestFailed();
TEST_EQUAL(request->GetData().size(), 0, (request->GetData()));
}
{
// cancel download in the middle of the progress
CancelDownload canceler;
// should be deleted in canceler
HttpRequest::Get(kTestUrlBigFile, bind(&CancelDownload::OnFinish, &canceler, _1),
bind(&CancelDownload::OnProgress, &canceler, _1));
QCoreApplication::exec();
}
observer.Reset();
{
// https success case
std::unique_ptr<HttpRequest> const request{MakeRequest("https://github.com")};
// wait until download is finished
QCoreApplication::exec();
observer.TestOk();
TEST_GREATER(request->GetData().size(), 0, ());
}
{
// Delete request at the end of successful download
DeleteOnFinish deleter;
// should be deleted in deleter on finish
HttpRequest::Get(kTestUrl1, bind(&DeleteOnFinish::OnFinish, &deleter, _1),
bind(&DeleteOnFinish::OnProgress, &deleter, _1));
QCoreApplication::exec();
}
}
// TODO: This test sometimes fails on CI. Reasons are unknown.
#ifndef OMIM_OS_MAC
UNIT_TEST(DownloaderSimplePost)
{
// simple success case
string const postData = "{\"jsonKey\":\"jsonValue\"}";
DownloadObserver observer;
std::unique_ptr<HttpRequest> const request{HttpRequest::PostJson(
"http://localhost:34568/unit_tests/post.php", postData, bind(&DownloadObserver::OnDownloadFinish, &observer, _1),
bind(&DownloadObserver::OnDownloadProgress, &observer, _1))};
// wait until download is finished
QCoreApplication::exec();
observer.TestOk();
TEST_EQUAL(request->GetData(), postData, ());
}
#endif
UNIT_TEST(ChunksDownloadStrategy)
{
vector<string> const servers = {"UrlOfServer1", "UrlOfServer2", "UrlOfServer3"};
typedef std::pair<int64_t, int64_t> RangeT;
RangeT const R1{0, 249}, R2{250, 499}, R3{500, 749}, R4{750, 800};
int64_t constexpr kFileSize = 800;
int64_t constexpr kChunkSize = 250;
ChunksDownloadStrategy strategy(servers);
strategy.InitChunks(kFileSize, kChunkSize);
string s1;
RangeT r1;
TEST_EQUAL(strategy.NextChunk(s1, r1), ChunksDownloadStrategy::ENextChunk, ());
string s2;
RangeT r2;
TEST_EQUAL(strategy.NextChunk(s2, r2), ChunksDownloadStrategy::ENextChunk, ());
string s3;
RangeT r3;
TEST_EQUAL(strategy.NextChunk(s3, r3), ChunksDownloadStrategy::ENextChunk, ());
string sEmpty;
RangeT rEmpty;
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
TEST(s1 != s2 && s2 != s3 && s3 != s1, (s1, s2, s3));
TEST(r1 != r2 && r2 != r3 && r3 != r1, (r1, r2, r3));
TEST(r1 == R1 || r1 == R2 || r1 == R3 || r1 == R4, (r1));
TEST(r2 == R1 || r2 == R2 || r2 == R3 || r2 == R4, (r2));
TEST(r3 == R1 || r3 == R2 || r3 == R3 || r3 == R4, (r3));
strategy.ChunkFinished(true, r1);
string s4;
RangeT r4;
TEST_EQUAL(strategy.NextChunk(s4, r4), ChunksDownloadStrategy::ENextChunk, ());
TEST_EQUAL(s4, s1, ());
TEST(r4 != r1 && r4 != r2 && r4 != r3, (r4));
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
strategy.ChunkFinished(false, r2);
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
strategy.ChunkFinished(true, r4);
string s5;
RangeT r5;
TEST_EQUAL(strategy.NextChunk(s5, r5), ChunksDownloadStrategy::ENextChunk, ());
TEST_EQUAL(s5, s4, (s5, s4));
TEST_EQUAL(r5, r2, ());
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
strategy.ChunkFinished(true, r5);
// 3rd is still alive here
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
strategy.ChunkFinished(true, r3);
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::EDownloadSucceeded, ());
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::EDownloadSucceeded, ());
}
UNIT_TEST(ChunksDownloadStrategyFAIL)
{
vector<string> const servers = {"UrlOfServer1", "UrlOfServer2"};
typedef std::pair<int64_t, int64_t> RangeT;
int64_t constexpr kFileSize = 800;
int64_t constexpr kChunkSize = 250;
ChunksDownloadStrategy strategy(servers);
strategy.InitChunks(kFileSize, kChunkSize);
string s1;
RangeT r1;
TEST_EQUAL(strategy.NextChunk(s1, r1), ChunksDownloadStrategy::ENextChunk, ());
string s2;
RangeT r2;
TEST_EQUAL(strategy.NextChunk(s2, r2), ChunksDownloadStrategy::ENextChunk, ());
TEST_EQUAL(strategy.NextChunk(s2, r2), ChunksDownloadStrategy::ENoFreeServers, ());
strategy.ChunkFinished(false, r1);
TEST_EQUAL(strategy.NextChunk(s2, r2), ChunksDownloadStrategy::ENoFreeServers, ());
strategy.ChunkFinished(false, r2);
TEST_EQUAL(strategy.NextChunk(s2, r2), ChunksDownloadStrategy::EDownloadFailed, ());
}
UNIT_TEST(ChunksDownloadStrategyDynamicChunks)
{
vector<string> const servers = {"UrlOfServer1", "UrlOfServer2"};
typedef std::pair<int64_t, int64_t> RangeT;
string url;
ChunksDownloadStrategy strategy(servers);
// Small 1MB file - one chunk
strategy.InitChunks(1024 * 1024, 0);
RangeT const R11{0, 1024 * 1024 - 1};
RangeT r1;
TEST_EQUAL(strategy.NextChunk(url, r1), ChunksDownloadStrategy::ENextChunk, ());
RangeT rEmpty;
TEST_EQUAL(strategy.NextChunk(url, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
TEST(r1 == R11, (r1));
// Small 1MB+1b file - 2 chunks
strategy = ChunksDownloadStrategy(servers);
strategy.InitChunks(1024 * 1024 + 1, 0);
RangeT const R21{0, 1024 * 1024 - 1}, R22{1024 * 1024, 1024 * 1024};
TEST_EQUAL(strategy.NextChunk(url, r1), ChunksDownloadStrategy::ENextChunk, ());
RangeT r2;
TEST_EQUAL(strategy.NextChunk(url, r2), ChunksDownloadStrategy::ENextChunk, ());
TEST_EQUAL(strategy.NextChunk(url, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
TEST(r1 == R21 && r2 == R22, (r1, r2));
// Big 200MB file - 5MB chunks
strategy = ChunksDownloadStrategy(servers);
strategy.InitChunks(200 * 1024 * 1024, 0);
RangeT const R31{0, 5 * 1024 * 1024 - 1}, R32{5 * 1024 * 1024, 2 * 5 * 1024 * 1024 - 1};
TEST_EQUAL(strategy.NextChunk(url, r1), ChunksDownloadStrategy::ENextChunk, ());
TEST_EQUAL(strategy.NextChunk(url, r2), ChunksDownloadStrategy::ENextChunk, ());
TEST(r1 == R31 && r2 == R32, (r1, r2));
}
namespace
{
string ReadFileAsString(string const & file)
{
try
{
FileReader f(file);
string s;
f.ReadAsString(s);
return s;
}
catch (FileReader::Exception const &)
{
TEST(false, ("File ", file, " should exist"));
return {};
}
} // namespace
void FinishDownloadSuccess(string const & file)
{
TEST(base::DeleteFileX(file), ("Result file should present on success"));
uint64_t size;
TEST(!base::GetFileSize(file + DOWNLOADING_FILE_EXTENSION, size), ("No downloading file on success"));
TEST(!base::GetFileSize(file + RESUME_FILE_EXTENSION, size), ("No resume file on success"));
}
void FinishDownloadFail(string const & file)
{
uint64_t size;
TEST(!base::GetFileSize(file, size), ("No result file on fail"));
(void)base::DeleteFileX(file + DOWNLOADING_FILE_EXTENSION);
TEST(base::DeleteFileX(file + RESUME_FILE_EXTENSION), ("Resume file should present on fail"));
}
void DeleteTempDownloadFiles()
{
// Remove data from previously failed files.
// Get regexp like this: (\.downloading3$|\.resume3$)
string const regexp = "(\\" RESUME_FILE_EXTENSION "$|\\" DOWNLOADING_FILE_EXTENSION "$)";
Platform::FilesList files;
Platform::GetFilesByRegExp(".", regexp, files);
for (Platform::FilesList::iterator it = files.begin(); it != files.end(); ++it)
FileWriter::DeleteFileX(*it);
}
} // namespace
UNIT_TEST(DownloadChunks)
{
string const kFileName = "some_downloader_test_file";
// remove data from previously failed files
DeleteTempDownloadFiles();
vector<string> urls = {kTestUrl1, kTestUrl1};
int64_t fileSize = 5;
DownloadObserver observer;
auto const MakeRequest = [&](int64_t chunkSize)
{
return HttpRequest::GetFile(urls, kFileName, fileSize, bind(&DownloadObserver::OnDownloadFinish, &observer, _1),
bind(&DownloadObserver::OnDownloadProgress, &observer, _1), chunkSize);
};
{
// should use only one thread
std::unique_ptr<HttpRequest> const request{MakeRequest(512 * 1024)};
// wait until download is finished
QCoreApplication::exec();
observer.TestOk();
TEST_EQUAL(request->GetData(), kFileName, ());
TEST_EQUAL(ReadFileAsString(kFileName), "Test1", ());
FinishDownloadSuccess(kFileName);
}
observer.Reset();
urls = {kTestUrlBigFile, kTestUrlBigFile, kTestUrlBigFile};
fileSize = 5;
{
// 3 threads - fail, because of invalid size
[[maybe_unused]] std::unique_ptr<HttpRequest> const request{MakeRequest(2048)};
// wait until download is finished
QCoreApplication::exec();
observer.TestFailed();
FinishDownloadFail(kFileName);
}
observer.Reset();
urls = {kTestUrlBigFile, kTestUrlBigFile, kTestUrlBigFile};
fileSize = kBigFileSize;
{
// 3 threads - succeeded
[[maybe_unused]] std::unique_ptr<HttpRequest> const request{MakeRequest(2048)};
// wait until download is finished
QCoreApplication::exec();
observer.TestOk();
FinishDownloadSuccess(kFileName);
}
observer.Reset();
urls = {kTestUrlBigFile, kTestUrl1, kTestUrl404};
fileSize = kBigFileSize;
{
// 3 threads with only one valid url - succeeded
[[maybe_unused]] std::unique_ptr<HttpRequest> const request{MakeRequest(2048)};
// wait until download is finished
QCoreApplication::exec();
observer.TestOk();
FinishDownloadSuccess(kFileName);
}
observer.Reset();
urls = {kTestUrlBigFile, kTestUrlBigFile};
fileSize = 12345;
{
// 2 threads and all points to file with invalid size - fail
[[maybe_unused]] std::unique_ptr<HttpRequest> const request{MakeRequest(2048)};
// wait until download is finished
QCoreApplication::exec();
observer.TestFailed();
FinishDownloadFail(kFileName);
}
}
namespace
{
int64_t constexpr beg1 = 123, end1 = 1230, beg2 = 44000, end2 = 47683;
struct ResumeChecker
{
size_t m_counter;
ResumeChecker() : m_counter(0) {}
void OnProgress(HttpRequest & request)
{
if (m_counter == 0)
{
TEST_EQUAL(request.GetProgress().m_bytesDownloaded, beg2, ());
TEST_EQUAL(request.GetProgress().m_bytesTotal, kBigFileSize, ());
}
else if (m_counter == 1)
{
TEST_EQUAL(request.GetProgress().m_bytesDownloaded, kBigFileSize, ());
TEST_EQUAL(request.GetProgress().m_bytesTotal, kBigFileSize, ());
}
else
{
TEST(false, ("Progress should be called exactly 2 times"));
}
++m_counter;
}
void OnFinish(HttpRequest & request)
{
TEST_EQUAL(request.GetStatus(), DownloadStatus::Completed, ());
QCoreApplication::exit();
}
};
} // namespace
UNIT_TEST(DownloadResumeChunks)
{
string const FILENAME = "some_test_filename_12345";
string const RESUME_FILENAME = FILENAME + RESUME_FILE_EXTENSION;
string const DOWNLOADING_FILENAME = FILENAME + DOWNLOADING_FILE_EXTENSION;
// remove data from previously failed files
DeleteTempDownloadFiles();
vector<string> urls = {kTestUrlBigFile};
// 1st step - download full file
{
DownloadObserver observer;
std::unique_ptr<HttpRequest> const request(
HttpRequest::GetFile(urls, FILENAME, kBigFileSize, bind(&DownloadObserver::OnDownloadFinish, &observer, _1),
bind(&DownloadObserver::OnDownloadProgress, &observer, _1)));
QCoreApplication::exec();
observer.TestOk();
uint64_t size;
TEST(!base::GetFileSize(RESUME_FILENAME, size), ("No resume file on success"));
}
// 2nd step - mark some file blocks as not downloaded
{
// to substitute temporary not fully downloaded file
TEST(base::RenameFileX(FILENAME, DOWNLOADING_FILENAME), ());
FileWriter f(DOWNLOADING_FILENAME, FileWriter::OP_WRITE_EXISTING);
f.Seek(beg1);
char b1[end1 - beg1 + 1] = {0};
f.Write(b1, ARRAY_SIZE(b1));
f.Seek(beg2);
char b2[end2 - beg2 + 1] = {0};
f.Write(b2, ARRAY_SIZE(b2));
ChunksDownloadStrategy strategy((vector<string>()));
strategy.AddChunk(std::make_pair(int64_t(0), beg1 - 1), ChunksDownloadStrategy::CHUNK_COMPLETE);
strategy.AddChunk(std::make_pair(beg1, end1), ChunksDownloadStrategy::CHUNK_FREE);
strategy.AddChunk(std::make_pair(end1 + 1, beg2 - 1), ChunksDownloadStrategy::CHUNK_COMPLETE);
strategy.AddChunk(std::make_pair(beg2, end2), ChunksDownloadStrategy::CHUNK_FREE);
strategy.SaveChunks(kBigFileSize, RESUME_FILENAME);
}
// 3rd step - check that resume works
{
ResumeChecker checker;
std::unique_ptr<HttpRequest> const request(HttpRequest::GetFile(urls, FILENAME, kBigFileSize,
bind(&ResumeChecker::OnFinish, &checker, _1),
bind(&ResumeChecker::OnProgress, &checker, _1)));
QCoreApplication::exec();
FinishDownloadSuccess(FILENAME);
}
}
// Unit test with forcible canceling of http request
UNIT_TEST(DownloadResumeChunksWithCancel)
{
string const FILENAME = "some_test_filename_12345";
// remove data from previously failed files
DeleteTempDownloadFiles();
vector<string> urls = {kTestUrlBigFile};
DownloadObserver observer;
int arrCancelChunks[] = {1, 3, 10, 15, 20, 0};
for (size_t i = 0; i < ARRAY_SIZE(arrCancelChunks); ++i)
{
if (arrCancelChunks[i] > 0)
observer.CancelDownloadOnGivenChunk(arrCancelChunks[i]);
std::unique_ptr<HttpRequest> const request(
HttpRequest::GetFile(urls, FILENAME, kBigFileSize, bind(&DownloadObserver::OnDownloadFinish, &observer, _1),
bind(&DownloadObserver::OnDownloadProgress, &observer, _1), 1024, false));
QCoreApplication::exec();
}
observer.TestOk();
FinishDownloadSuccess(FILENAME);
}
} // namespace downloader_test