Files
UnrealEngineUWP/Engine/Source/Programs/WebTests/Private/TestHttp.cpp
lorry li 3fc1d2becf Make sure progress callback can be called on http thread;
Make sure HttpManagerRequestAdded and HttpManagerRequestCompleted callback can be called on http thread.

From now on the delegates bind to http manager through SetRequestAddedDelegate and SetRequestCompletedDelegate have to be changed to be thread-safe.

#jira UE-182677
[REVIEW] [at]michael.atchison [at]michael.kirzinger [at]rafa.lecina [at]justin.marcus

[CL 27506980 by lorry li in ue5-main branch]
2023-08-30 18:56:04 -04:00

916 lines
31 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "CoreMinimal.h"
#include "HAL/FileManager.h"
#include "HAL/IConsoleManager.h"
#include "HAL/PlatformProcess.h"
#include "HAL/Runnable.h"
#include "HAL/RunnableThread.h"
#include "HttpManager.h"
#include "HttpRetrySystem.h"
#include "Http.h"
#include "Misc/CommandLine.h"
#include "TestHarness.h"
/**
* HTTP Tests
* -----------------------------------------------------------------------------------------------
*
* PURPOSE:
*
* Integration Tests to make sure all kinds of HTTP client features in C++ work well on different platforms,
* including but not limited to error handing, retrying, threading, streaming, SSL and profiling.
*
* Refer to WebTests/README.md for more info about how to run these tests
*
* -----------------------------------------------------------------------------------------------
*/
#define HTTP_TAG "[HTTP]"
#define HTTP_TIME_DIFF_TOLERANCE 0.5f
class FHttpModuleTestFixture
{
public:
FHttpModuleTestFixture()
: WebServerIp(TEXT("127.0.0.1"))
, WebServerHttpPort(8000)
, bRunHeavyTests(false)
{
ParseSettingsFromCommandLine();
HttpModule = new FHttpModule();
IModuleInterface* Module = HttpModule;
Module->StartupModule();
}
virtual ~FHttpModuleTestFixture()
{
IModuleInterface* Module = HttpModule;
Module->ShutdownModule();
delete Module;
}
void ParseSettingsFromCommandLine()
{
FParse::Value(FCommandLine::Get(), TEXT("web_server_ip"), WebServerIp);
FParse::Bool(FCommandLine::Get(), TEXT("run_heavy_tests"), bRunHeavyTests);
}
const FString UrlWithInvalidPortToTestConnectTimeout() const { return FString::Format(TEXT("http://{0}:{1}"), { *WebServerIp, 8765 }); }
const FString UrlBase() const { return FString::Format(TEXT("http://{0}:{1}"), { *WebServerIp, WebServerHttpPort }); }
const FString UrlHttpTests() const { return FString::Format(TEXT("{0}/webtests/httptests"), { *UrlBase() }); }
const FString UrlToTestMethods() const { return FString::Format(TEXT("{0}/methods"), { *UrlHttpTests() }); }
const FString UrlStreamDownload(uint32 Chunks, uint32 ChunkSize) { return FString::Format(TEXT("{0}/streaming_download/{1}/{2}/"), { *UrlHttpTests(), Chunks, ChunkSize }); }
FString WebServerIp;
uint32 WebServerHttpPort;
FHttpModule* HttpModule;
bool bRunHeavyTests;
};
TEST_CASE_METHOD(FHttpModuleTestFixture, "Shutdown http module without issue when there are ongoing http requests.", HTTP_TAG)
{
uint32 ChunkSize = 1024 * 1024;
TArray<uint8> DataChunk;
DataChunk.SetNum(ChunkSize);
FMemory::Memset(DataChunk.GetData(), 'd', ChunkSize);
for (int32 i = 0; i < 10; ++i)
{
IHttpRequest* LeakingHttpRequest = FPlatformHttp::ConstructRequest(); // Leaking in purpose to make sure it's ok
TSharedRef<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetURL(UrlToTestMethods());
HttpRequest->SetVerb(TEXT("PUT"));
// TODO: Use some shared data, like cookie, openssl session etc.
HttpRequest->SetContent(DataChunk);
HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(bSucceeded);
});
HttpRequest->ProcessRequest();
}
HttpModule->GetHttpManager().Tick(0.0f);
}
class FWaitUntilQuitFromTestFixture : public FHttpModuleTestFixture
{
public:
FWaitUntilQuitFromTestFixture()
{
}
~FWaitUntilQuitFromTestFixture()
{
WaitUntilQuitFromTest();
}
void WaitUntilQuitFromTest()
{
while (!bQuitRequested)
{
HttpModule->GetHttpManager().Tick(TickFrequency);
FPlatformProcess::Sleep(TickFrequency);
}
}
float TickFrequency = 1.0f / 60; /*60 FPS*/;
bool bQuitRequested = false;
};
TEST_CASE_METHOD(FWaitUntilQuitFromTestFixture, "Http request can be reused", HTTP_TAG)
{
TSharedRef<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetURL(UrlToTestMethods());
HttpRequest->SetVerb(TEXT("POST"));
HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(bSucceeded);
CHECK(HttpResponse->GetResponseCode() == 200);
uint32 Chunks = 3;
uint32 ChunkSize = 1024;
HttpRequest->SetURL(UrlStreamDownload(Chunks, ChunkSize));
HttpRequest->SetVerb(TEXT("GET"));
HttpRequest->OnProcessRequestComplete().BindLambda([this, Chunks, ChunkSize](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(bSucceeded);
REQUIRE(HttpResponse != nullptr);
CHECK(HttpResponse->GetResponseCode() == 200);
CHECK(HttpResponse->GetContentLength() == Chunks * ChunkSize);
bQuitRequested = true;
});
HttpRequest->ProcessRequest();
});
HttpRequest->ProcessRequest();
}
class FWaitUntilCompleteHttpFixture : public FHttpModuleTestFixture
{
public:
FWaitUntilCompleteHttpFixture()
{
HttpModule->GetHttpManager().SetRequestAddedDelegate(FHttpManagerRequestAddedDelegate::CreateRaw(this, &FWaitUntilCompleteHttpFixture::OnRequestAdded));
HttpModule->GetHttpManager().SetRequestCompletedDelegate(FHttpManagerRequestCompletedDelegate::CreateRaw(this, &FWaitUntilCompleteHttpFixture::OnRequestCompleted));
}
~FWaitUntilCompleteHttpFixture()
{
WaitUntilAllHttpRequestsComplete();
HttpModule->GetHttpManager().SetRequestAddedDelegate(FHttpManagerRequestAddedDelegate());
HttpModule->GetHttpManager().SetRequestCompletedDelegate(FHttpManagerRequestCompletedDelegate());
}
void OnRequestAdded(const FHttpRequestRef& Request)
{
++OngoingRequests;
}
void OnRequestCompleted(const FHttpRequestRef& Request)
{
ensure(--OngoingRequests >= 0);
}
void WaitUntilAllHttpRequestsComplete()
{
while (OngoingRequests != 0)
{
HttpModule->GetHttpManager().Tick(TickFrequency);
FPlatformProcess::Sleep(TickFrequency);
}
}
std::atomic<int32> OngoingRequests = 0;
float TickFrequency = 1.0f / 60; /*60 FPS*/;
};
TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http Methods", HTTP_TAG)
{
TSharedRef<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetURL(UrlToTestMethods());
SECTION("GET")
{
HttpRequest->SetVerb(TEXT("GET"));
}
SECTION("POST")
{
HttpRequest->SetVerb(TEXT("POST"));
}
SECTION("PUT")
{
HttpRequest->SetVerb(TEXT("PUT"));
}
SECTION("DELETE")
{
HttpRequest->SetVerb(TEXT("DELETE"));
}
HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(bSucceeded);
REQUIRE(HttpResponse != nullptr);
CHECK(HttpResponse->GetResponseCode() == 200);
});
HttpRequest->ProcessRequest();
}
TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Get large response content without chunks", HTTP_TAG)
{
TSharedRef<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetURL(FString::Format(TEXT("{0}/get_large_response_without_chunks/{1}/"), { *UrlHttpTests(), 1024 * 1024/*bytes_number*/}));
HttpRequest->SetVerb(TEXT("GET"));
HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(bSucceeded);
REQUIRE(HttpResponse != nullptr);
CHECK(HttpResponse->GetResponseCode() == 200);
});
HttpRequest->ProcessRequest();
}
// TODO: Enable this after finding a more reliable way of simulate timeout
//TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http request connect timeout", HTTP_TAG)
//{
// TSharedRef<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
// HttpRequest->SetURL(UrlWithInvalidPortToTestConnectTimeout());
// HttpRequest->SetVerb(TEXT("GET"));
// HttpRequest->SetTimeout(7);
// FDateTime StartTime = FDateTime::Now();
// HttpRequest->OnProcessRequestComplete().BindLambda([StartTime](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
// CHECK(!bSucceeded);
// CHECK(HttpResponse == nullptr);
// // TODO: For now curl impl is using customized timeout instead of relying on native http timeout,
// // which doesn't get CURLE_COULDNT_CONNECT. Enable this after switching to native http timeout
// //CHECK(HttpRequest->GetStatus() == EHttpRequestStatus::Failed_ConnectionError);
// FTimespan Timespan = FDateTime::Now() - StartTime;
// float DurationInSeconds = Timespan.GetTotalSeconds();
// CHECK(FMath::IsNearlyEqual(DurationInSeconds, 7, HTTP_TIME_DIFF_TOLERANCE));
// });
// HttpRequest->ProcessRequest();
//}
TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Streaming http download", HTTP_TAG)
{
uint32 Chunks = 3;
uint32 ChunkSize = 1024*1024;
TSharedRef<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetURL(UrlStreamDownload(Chunks, ChunkSize));
HttpRequest->SetVerb(TEXT("GET"));
TSharedRef<int64> TotalBytesReceived = MakeShared<int64>(0);
SECTION("Success without stream provided")
{
HttpRequest->OnProcessRequestComplete().BindLambda([Chunks, ChunkSize](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(bSucceeded);
REQUIRE(HttpResponse != nullptr);
CHECK(HttpResponse->GetResponseCode() == 200);
CHECK(!HttpResponse->GetAllHeaders().IsEmpty());
CHECK(HttpResponse->GetContentLength() == Chunks * ChunkSize);
});
}
SECTION("Success with customized stream")
{
class FTestHttpReceiveStream final : public FArchive
{
public:
FTestHttpReceiveStream(TSharedRef<int64> InTotalBytesReceived)
: TotalBytesReceived(InTotalBytesReceived)
{
}
virtual void Serialize(void* V, int64 Length) override
{
*TotalBytesReceived += Length;
}
TSharedRef<int64> TotalBytesReceived;
};
TSharedRef<FTestHttpReceiveStream> Stream = MakeShared<FTestHttpReceiveStream>(TotalBytesReceived);
CHECK(HttpRequest->SetResponseBodyReceiveStream(Stream));
HttpRequest->OnProcessRequestComplete().BindLambda([Chunks, ChunkSize, TotalBytesReceived](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(bSucceeded);
REQUIRE(HttpResponse != nullptr);
CHECK(HttpResponse->GetResponseCode() == 200);
CHECK(!HttpResponse->GetAllHeaders().IsEmpty());
CHECK(HttpResponse->GetContentLength() == Chunks * ChunkSize);
CHECK(HttpResponse->GetContent().IsEmpty());
CHECK(*TotalBytesReceived == Chunks * ChunkSize);
});
}
SECTION("Success with customized stream delegate")
{
FHttpRequestStreamDelegate Delegate;
Delegate.BindLambda([TotalBytesReceived](void* Ptr, int64 Length) {
*TotalBytesReceived += Length;
return true;
});
CHECK(HttpRequest->SetResponseBodyReceiveStreamDelegate(Delegate));
HttpRequest->OnProcessRequestComplete().BindLambda([Chunks, ChunkSize, TotalBytesReceived](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(bSucceeded);
REQUIRE(HttpResponse != nullptr);
CHECK(HttpResponse->GetResponseCode() == 200);
CHECK(!HttpResponse->GetAllHeaders().IsEmpty());
CHECK(HttpResponse->GetContentLength() == Chunks * ChunkSize);
CHECK(HttpResponse->GetContent().IsEmpty());
CHECK(*TotalBytesReceived == Chunks * ChunkSize);
});
}
SECTION("Use customized stream to receive response body but failed when serialize")
{
class FTestHttpReceiveStream final : public FArchive
{
public:
FTestHttpReceiveStream(TSharedRef<int64> InTotalBytesReceived)
: TotalBytesReceived(InTotalBytesReceived)
{
}
virtual void Serialize(void* V, int64 Length) override
{
*TotalBytesReceived += Length;
SetError();
}
TSharedRef<int64> TotalBytesReceived;
};
TSharedRef<FTestHttpReceiveStream> Stream = MakeShared<FTestHttpReceiveStream>(TotalBytesReceived);
CHECK(HttpRequest->SetResponseBodyReceiveStream(Stream));
HttpRequest->OnProcessRequestComplete().BindLambda([ChunkSize, TotalBytesReceived](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(!bSucceeded);
CHECK(HttpResponse != nullptr);
CHECK(*TotalBytesReceived <= ChunkSize);
});
}
SECTION("Use customized stream delegate to receive response body but failed when call")
{
FHttpRequestStreamDelegate Delegate;
Delegate.BindLambda([TotalBytesReceived](void* Ptr, int64 Length) {
*TotalBytesReceived += Length;
return false;
});
CHECK(HttpRequest->SetResponseBodyReceiveStreamDelegate(Delegate));
HttpRequest->OnProcessRequestComplete().BindLambda([ChunkSize, TotalBytesReceived](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(!bSucceeded);
CHECK(HttpResponse != nullptr);
CHECK(*TotalBytesReceived <= ChunkSize);
});
}
SECTION("Success with file stream to receive response body")
{
FString Filename = FString(FPlatformProcess::UserSettingsDir()) / TEXT("TestStreamDownload.dat");
FArchive* RawFile = IFileManager::Get().CreateFileWriter(*Filename);
CHECK(RawFile != nullptr);
TSharedRef<FArchive> FileToWrite = MakeShareable(RawFile);
CHECK(HttpRequest->SetResponseBodyReceiveStream(FileToWrite));
HttpRequest->OnProcessRequestComplete().BindLambda([Chunks, ChunkSize, Filename, FileToWrite](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(bSucceeded);
REQUIRE(HttpResponse != nullptr);
CHECK(HttpResponse->GetContentLength() == Chunks * ChunkSize);
CHECK(HttpResponse->GetContent().IsEmpty());
CHECK(HttpResponse->GetResponseCode() == 200);
CHECK(!HttpResponse->GetAllHeaders().IsEmpty());
FileToWrite->FlushCache();
FileToWrite->Close();
TSharedRef<FArchive> FileToRead = MakeShareable(IFileManager::Get().CreateFileReader(*Filename));
CHECK(FileToRead->TotalSize() == Chunks * ChunkSize);
IFileManager::Get().Delete(*Filename);
});
}
HttpRequest->ProcessRequest();
}
TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Can run parallel stream download requests", HTTP_TAG)
{
uint32 Chunks = 5;
uint32 ChunkSize = 1024*1024;
for (int i = 0; i < 3; ++i)
{
TSharedRef<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetURL(UrlStreamDownload(Chunks, ChunkSize));
HttpRequest->SetVerb(TEXT("GET"));
HttpRequest->OnProcessRequestComplete().BindLambda([Chunks, ChunkSize](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(HttpResponse->GetContentLength() == Chunks * ChunkSize);
CHECK(bSucceeded);
CHECK(HttpResponse->GetResponseCode() == 200);
});
HttpRequest->ProcessRequest();
}
}
TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Can download big file exceeds 32 bits", HTTP_TAG)
{
if (!bRunHeavyTests)
{
return;
}
// 5 * 1024 * 1024 * 1024 BYTES = 5368709120 BYTES = 5 GB
uint64 Chunks = 5 * 1024;
uint64 ChunkSize = 1024 * 1024;
TSharedRef<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetURL(UrlStreamDownload(Chunks, ChunkSize));
HttpRequest->SetVerb(TEXT("GET"));
TSharedRef<int64> TotalBytesReceived = MakeShared<int64>(0);
FHttpRequestStreamDelegate Delegate;
Delegate.BindLambda([TotalBytesReceived](void* Ptr, int64 Length) {
*TotalBytesReceived += Length;
return true;
});
HttpRequest->SetResponseBodyReceiveStreamDelegate(Delegate);
HttpRequest->OnProcessRequestComplete().BindLambda([Chunks, ChunkSize, TotalBytesReceived](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(bSucceeded);
REQUIRE(HttpResponse != nullptr);
CHECK(HttpResponse->GetContentLength() == Chunks * ChunkSize);
CHECK(HttpResponse->GetContent().IsEmpty());
CHECK(*TotalBytesReceived == Chunks * ChunkSize);
CHECK(HttpResponse->GetResponseCode() == 200);
});
HttpRequest->ProcessRequest();
}
TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Streaming http upload from memory", HTTP_TAG)
{
TSharedRef<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetURL(FString::Format(TEXT("{0}/streaming_upload_post"), { *UrlHttpTests() }));
HttpRequest->SetVerb(TEXT("POST"));
const char* BoundaryLabel = "test_http_boundary";
HttpRequest->SetHeader(TEXT("Content-Type"), FString::Format(TEXT("multipart/form-data; boundary={0}"), { BoundaryLabel }));
// Data will be sent by chunks in http request
const uint32 FileSize = 10*1024*1024;
char* FileData = (char*)FMemory::Malloc(FileSize + 1);
FMemory::Memset(FileData, 'd', FileSize);
FileData[FileSize + 1] = '\0';
TArray<uint8> ContentData;
const int32 ContentMaxSize = FileSize + 256/*max length of format string*/;
ContentData.Reserve(ContentMaxSize);
const int32 ContentLength = FCStringAnsi::Snprintf(
(char*)ContentData.GetData(),
ContentMaxSize,
"--%s\r\n"
"Content-Disposition: form-data; name=\"file\"; filename=\"bigfile.zip\"\r\n"
"Content-Type: application/octet-stream\r\n\r\n"
"%s\r\n"
"--%s--",
BoundaryLabel, FileData, BoundaryLabel);
FMemory::Free(FileData);
CHECK(ContentLength > 0);
CHECK(ContentLength < ContentMaxSize);
ContentData.SetNumUninitialized(ContentLength);
HttpRequest->SetContent(MoveTemp(ContentData));
HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(bSucceeded);
REQUIRE(HttpResponse != nullptr);
CHECK(HttpResponse->GetResponseCode() == 200);
});
HttpRequest->ProcessRequest();
}
class FTestHttpUploadStream final : public FArchive
{
public:
FTestHttpUploadStream(uint64 InTotalSize)
: FakeTotalSize(InTotalSize)
{
}
virtual void Serialize(void* V, int64 Length) override
{
for (int64 i = 0; i < Length; ++i)
{
((char*)V)[i] = 'd';
}
CurrentPos += Length;
}
virtual int64 TotalSize() override
{
return FakeTotalSize;
}
virtual void Seek(int64 InPos)
{
CurrentPos = InPos;
}
virtual int64 Tell() override
{
return CurrentPos;
}
uint64 FakeTotalSize;
uint64 CurrentPos = 0;
};
TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Can upload big file exceeds 32 bits", HTTP_TAG)
{
if (!bRunHeavyTests)
{
return;
}
// TODO: Back to check later. xCurl 2206.4.0.0 doesn't work with file bigger than 32 bits
// 5 * 1024 * 1024 * 1024 BYTES = 5368709120 BYTES = 5 GB
//const uint64 TotalSize = 5368709120;
//const uint64 TotalSize = 4294967296;
//const uint64 TotalSize = 4294967295;
//const uint64 TotalSize = 2147483648;
const uint64 TotalSize = 2147483647;
TSharedRef<FTestHttpUploadStream> Stream = MakeShared<FTestHttpUploadStream>(TotalSize);
TSharedRef<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetURL(FString::Format(TEXT("{0}/streaming_upload_put"), { *UrlHttpTests() }));
HttpRequest->SetVerb(TEXT("PUT"));
HttpRequest->SetContentFromStream(Stream);
HttpRequest->SetHeader(TEXT("Content-Disposition"), TEXT("attachment;filename=TestStreamUpload.dat"));
HttpRequest->OnProcessRequestComplete().BindLambda([Stream, TotalSize](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(bSucceeded);
REQUIRE(HttpResponse != nullptr);
CHECK(HttpResponse->GetResponseCode() == 200);
CHECK(Stream->CurrentPos == TotalSize);
});
HttpRequest->ProcessRequest();
}
TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Streaming http upload from file by PUT can work well", HTTP_TAG)
{
FString Filename = FString(FPlatformProcess::UserSettingsDir()) / TEXT("TestStreamUpload.dat");
FArchive* RawFile = IFileManager::Get().CreateFileWriter(*Filename);
CHECK(RawFile != nullptr);
TSharedRef<FArchive> FileToWrite = MakeShareable(RawFile);
const uint64 FileSize = 5*1024*1024; // 5MB
char* FileData = (char*)FMemory::Malloc(FileSize);
FMemory::Memset(FileData, 'd', FileSize);
FileToWrite->Serialize(FileData, FileSize);
FileToWrite->FlushCache();
FileToWrite->Close();
FMemory::Free(FileData);
TSharedRef<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetURL(FString::Format(TEXT("{0}/streaming_upload_put"), { *UrlHttpTests() }));
HttpRequest->SetVerb(TEXT("PUT"));
HttpRequest->SetHeader(TEXT("Content-Disposition"), TEXT("attachment;filename=TestStreamUpload.dat"));
HttpRequest->SetContentAsStreamedFile(Filename);
HttpRequest->OnProcessRequestComplete().BindLambda([Filename](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(bSucceeded);
CHECK(HttpResponse->GetResponseCode() == 200);
IFileManager::Get().Delete(*Filename);
});
HttpRequest->ProcessRequest();
}
TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Redirect enabled by default and can work well", HTTP_TAG)
{
TSharedRef<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetURL(FString::Format(TEXT("{0}/redirect_from"), { *UrlHttpTests() }));
HttpRequest->SetVerb(TEXT("GET"));
HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(bSucceeded);
CHECK(HttpResponse->GetResponseCode() == 200);
});
HttpRequest->ProcessRequest();
}
class FThreadedHttpRunnable : public FRunnable
{
public:
DECLARE_DELEGATE(FRunActualTestCodeDelegate);
FRunActualTestCodeDelegate& OnRunFromThread()
{
return ThreadCallback;
}
// FRunnable interface
virtual uint32 Run() override
{
ThreadCallback.ExecuteIfBound();
return 0;
}
void StartTestHttpThread(bool bBlockGameThread)
{
bBlockingGameThreadTick = bBlockGameThread;
RunnableThread = TSharedPtr<FRunnableThread>(FRunnableThread::Create(this, TEXT("Test Http Thread")));
while (bBlockingGameThreadTick)
{
float TickFrequency = 1.0f / 60; /*60 FPS*/;
FPlatformProcess::Sleep(TickFrequency);
}
}
void UnblockGameThread()
{
bBlockingGameThreadTick = false;
}
private:
FRunActualTestCodeDelegate ThreadCallback;
TSharedPtr<FRunnableThread> RunnableThread;
std::atomic<bool> bBlockingGameThreadTick = true;
};
class FWaitThreadedHttpFixture : public FWaitUntilCompleteHttpFixture
{
public:
~FWaitThreadedHttpFixture()
{
WaitUntilAllHttpRequestsComplete();
}
FThreadedHttpRunnable ThreadedHttpRunnable;
};
TEST_CASE_METHOD(FWaitThreadedHttpFixture, "Http streaming download request can work in non game thread", HTTP_TAG)
{
ThreadedHttpRunnable.OnRunFromThread().BindLambda([this]() {
TSharedRef<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetURL(UrlStreamDownload(3/*Chunks*/, 1024/*ChunkSize*/));
HttpRequest->SetVerb(TEXT("GET"));
HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread);
class FTestHttpReceiveStream final : public FArchive
{
public:
virtual void Serialize(void* V, int64 Length) override
{
// No matter what's the thread policy, Serialize always get called in http thread.
CHECK(!IsInGameThread());
}
};
CHECK(HttpRequest->SetResponseBodyReceiveStream(MakeShared<FTestHttpReceiveStream>()));
HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
// EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread was used, so not in game thread here
CHECK(!IsInGameThread());
CHECK(bSucceeded);
REQUIRE(HttpResponse != nullptr);
CHECK(HttpResponse->GetResponseCode() == 200);
CHECK(!HttpResponse->GetAllHeaders().IsEmpty());
ThreadedHttpRunnable.UnblockGameThread();
});
HttpRequest->ProcessRequest();
});
ThreadedHttpRunnable.StartTestHttpThread(true/*bBlockGameThread*/);
}
TEST_CASE_METHOD(FWaitThreadedHttpFixture, "Http download request progress callback can be received in http thread", HTTP_TAG)
{
std::atomic<bool> bRequestProgressTriggered = false;
ThreadedHttpRunnable.OnRunFromThread().BindLambda([this, &bRequestProgressTriggered]() {
TSharedRef<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetURL(UrlStreamDownload(10/*Chunks*/, 1024*1024/*ChunkSize*/));
HttpRequest->SetVerb(TEXT("GET"));
HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread);
HttpRequest->OnRequestProgress64().BindLambda([this, &bRequestProgressTriggered](FHttpRequestPtr Request, uint64 /*BytesSent*/, uint64 BytesReceived) {
if (!bRequestProgressTriggered)
{
// Only do these checks once, because when http request complete, this callback also get triggered
CHECK(BytesReceived > 0);
CHECK(BytesReceived < 10/*Chunks*/ * 1024*1024/*ChunkSize*/);
CHECK(!IsInGameThread());
CHECK(Request->GetStatus() == EHttpRequestStatus::Processing);
bRequestProgressTriggered = true;
}
});
HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr /*HttpRequest*/, FHttpResponsePtr /*HttpResponse */, bool bSucceeded) {
CHECK(bSucceeded);
ThreadedHttpRunnable.UnblockGameThread();
});
HttpRequest->ProcessRequest();
});
ThreadedHttpRunnable.StartTestHttpThread(true/*bBlockGameThread*/);
CHECK(bRequestProgressTriggered);
}
namespace UE
{
namespace TestHttp
{
void SetupURLRequestFilter(FHttpModule* HttpModule)
{
// Pre check will fail when domain is not allowed
UE::Core::FURLRequestFilter::FRequestMap SchemeMap;
SchemeMap.Add(TEXT("http"), TArray<FString>{TEXT("epicgames.com")});
UE::Core::FURLRequestFilter Filter{SchemeMap};
HttpModule->GetHttpManager().SetURLRequestFilter(Filter);
}
}
}
TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Http request pre check will fail", HTTP_TAG)
{
// Pre check will fail when domain is not allowed
UE::TestHttp::SetupURLRequestFilter(HttpModule);
TSharedRef<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetVerb(TEXT("GET"));
HttpRequest->SetURL(UrlToTestMethods());
SECTION("on game thread")
{
HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(IsInGameThread());
CHECK(!bSucceeded);
});
}
SECTION("on http thread")
{
HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread);
HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(!IsInGameThread());
CHECK(!bSucceeded);
});
}
HttpRequest->ProcessRequest();
}
TEST_CASE_METHOD(FWaitThreadedHttpFixture, "Threaded http request pre check will fail", HTTP_TAG)
{
ThreadedHttpRunnable.OnRunFromThread().BindLambda([this]() {
// Pre check will fail when domain is not allowed
UE::TestHttp::SetupURLRequestFilter(HttpModule);
TSharedRef<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetVerb(TEXT("GET"));
HttpRequest->SetURL(UrlToTestMethods());
SECTION("on game thread")
{
HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(IsInGameThread());
CHECK(!bSucceeded);
});
}
SECTION("on http thread")
{
HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread);
HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(!IsInGameThread());
CHECK(!bSucceeded);
});
}
HttpRequest->ProcessRequest();
});
ThreadedHttpRunnable.StartTestHttpThread(false/*bBlockGameThread*/);
}
TEST_CASE_METHOD(FWaitUntilCompleteHttpFixture, "Cancel http request connect before timeout", HTTP_TAG)
{
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetURL(UrlWithInvalidPortToTestConnectTimeout());
HttpRequest->SetVerb(TEXT("GET"));
HttpRequest->SetTimeout(7);
FDateTime StartTime = FDateTime::Now();
HttpRequest->OnProcessRequestComplete().BindLambda([StartTime](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
CHECK(!bSucceeded);
FTimespan Timespan = FDateTime::Now() - StartTime;
float DurationInSeconds = Timespan.GetTotalSeconds();
CHECK(DurationInSeconds < 2);
});
HttpRequest->ProcessRequest();
FPlatformProcess::Sleep(0.5);
HttpRequest->CancelRequest();
}
class FMockRetryManager : public FHttpRetrySystem::FManager
{
public:
using FHttpRetrySystem::FManager::FManager;
using FHttpRetrySystem::FManager::RequestList;
using FHttpRetrySystem::FManager::FHttpRetryRequestEntry;
using FHttpRetrySystem::FManager::RetryTimeoutRelativeSecondsDefault;
};
class FRetryHttpFixture : public FWaitUntilCompleteHttpFixture
{
public:
FRetryHttpFixture()
{
HttpRetryManager = MakeShared<FMockRetryManager>(FHttpRetrySystem::FRetryLimitCountSetting(RetryLimitCount), FHttpRetrySystem::FRetryTimeoutRelativeSecondsSetting(/*RetryTimeoutRelativeSeconds*/));
HttpRetryManager->RetryTimeoutRelativeSecondsDefault = 2.0f; // Value is set so that retry system will use it at high level, will no longer wait for ever for HTTP code
}
~FRetryHttpFixture()
{
// Make sure not destroy retry manager before waiting in FWaitUntilCompleteHttpFixture
while (HttpRetryManager->RequestList.Num())
{
HttpRetryManager->Update();
HttpModule->GetHttpManager().Tick(TickFrequency);
FPlatformProcess::Sleep(TickFrequency);
}
}
TSharedPtr<FMockRetryManager> HttpRetryManager;
uint32 RetryLimitCount = 2;
FThreadedHttpRunnable ThreadedHttpRunnable;
};
// TODO: Currently broken, will fix this in retry system
//TEST_CASE_METHOD(FRetryHttpFixture, "Http request adaptor in retry system can work in non game thread", HTTP_TAG)
//{
// ThreadedHttpRunnable.OnRunFromThread().BindLambda([this]() {
// TSharedRef<IHttpRequest> HttpRequest = HttpRetryManager->CreateRequest();
// HttpRequest->SetURL(UrlStreamDownload(3/*Chunks*/, 1024/*ChunkSize*/));
// HttpRequest->SetVerb(TEXT("GET"));
// HttpRequest->SetDelegateThreadPolicy(EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread);
//
// class FTestHttpReceiveStream final : public FArchive
// {
// public:
// virtual void Serialize(void* V, int64 Length) override
// {
// // No matter what's the thread policy, Serialize always get called in http thread.
// CHECK(!IsInGameThread());
// }
// };
// CHECK(HttpRequest->SetResponseBodyReceiveStream(MakeShared<FTestHttpReceiveStream>()));
//
// HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) {
// // EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread was used, so not in game thread here
// CHECK(!IsInGameThread());
// CHECK(bSucceeded);
// CHECK(HttpResponse->GetResponseCode() == 200);
// ThreadedHttpRunnable.UnblockGameThread();
// });
//
// HttpRequest->ProcessRequest();
// });
//
// ThreadedHttpRunnable.StartTestHttpThread(true/*bBlockGameThread*/);
//}
class FRetryHttpManagerThreadedRequestsFixture : public FRetryHttpFixture
{
public:
void LaunchDownloadRequests()
{
for (int32 i = 0; i < 10; ++i)
{
TSharedRef<IHttpRequest> HttpRequest = HttpRetryManager->CreateRequest();
HttpRequest->SetURL(UrlStreamDownload(3, 1024*1024));
HttpRequest->SetVerb(TEXT("GET"));
HttpRequest->ProcessRequest();
}
}
};
TEST_CASE_METHOD(FRetryHttpManagerThreadedRequestsFixture, "Retry manager is thread safe", HTTP_TAG)
{
ThreadedHttpRunnable.OnRunFromThread().BindLambda([this]() {
LaunchDownloadRequests();
HttpRetryManager->BlockUntilFlushed(5.0);
});
ThreadedHttpRunnable.StartTestHttpThread(false/*bBlockGameThread*/);
LaunchDownloadRequests();
HttpRetryManager->BlockUntilFlushed(5.0);
}
// TODO: Add cancel test, with multiple cancel calls
// TODO: Add a test case to make sure once Request completed and destroyed, if the user code still keeps the shared ptr of Response, any call to Response shouldn't have dependency to Request
// TODO: Add test case to validate header received callback can be received in http/game thread, and can be received before the request complete