diff --git a/client/http_curl.cpp b/client/http_curl.cpp index 96a8228c61c..30c52c9c76e 100644 --- a/client/http_curl.cpp +++ b/client/http_curl.cpp @@ -1,6 +1,6 @@ // This file is part of BOINC. -// http://boinc.berkeley.edu -// Copyright (C) 2008 University of California +// https://boinc.berkeley.edu +// Copyright (C) 2025 University of California // // BOINC is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License @@ -55,159 +55,62 @@ using std::min; using std::vector; -static CURLM* g_curlMulti = NULL; -static char g_user_agent_string[256] = {""}; -static unsigned int g_trace_count = 0; -static bool got_expectation_failed = false; - // Whether we've got a 417 HTTP error. - // If we did, it's probably because we talked HTTP 1.1 to a 1.0 proxy; - // use 1.0 from now on. - -static void get_user_agent_string() { - if (g_user_agent_string[0]) return; - snprintf(g_user_agent_string, sizeof(g_user_agent_string), - "BOINC client (%s %d.%d.%d)", - HOSTTYPE, - BOINC_MAJOR_VERSION, BOINC_MINOR_VERSION, BOINC_RELEASE - ); - if (strlen(gstate.client_brand)) { - char buf[1024]; - snprintf(buf, sizeof(buf), " (%s)", gstate.client_brand); - safe_strcat(g_user_agent_string, buf); - } +HTTP_CURL::HTTP_CURL() : + curlMulti(curl_init()), + user_agent_string(std::move(build_user_agent_string())), + trace_count(0), + use_http_1_0(false) { } -size_t libcurl_write(void *ptr, size_t size, size_t nmemb, HTTP_OP* phop) { - // take the stream param as a FILE* and write to disk - // TODO: maybe assert stRead == size*nmemb, - // add exception handling on phop members - // - size_t stWrite = fwrite(ptr, size, nmemb, phop->fileOut); - if (log_flags.http_xfer_debug) { - msg_printf(NULL, MSG_INFO, - "[http_xfer] [ID#%d] HTTP: wrote %d bytes", phop->trace_id, (int)stWrite - ); - } - phop->bytes_xferred += (double)(stWrite); - phop->update_speed(); // this should update the transfer speed - daily_xfer_history.add(stWrite, false); - return stWrite; +HTTP_CURL::~HTTP_CURL() { + curl_cleanup(); } -size_t libcurl_read(void *ptr, size_t size, size_t nmemb, HTTP_OP* phop) { - // read data from inFile (or from header if present) - // and move to buffer for Curl to send it. - // - size_t stSend = size * nmemb; - int stRead = 0; - - if (phop->req1 && !phop->bSentHeader) { - // need to send headers first, then data file - // so requests from 0 to strlen(req1)-1 are from memory, - // and from strlen(req1) to content_length are from the file - if (phop->lSeek < (long) strlen(phop->req1)) { - // need to read header, either just starting to read - // (i.e. this is the first time in this function for this phop) - // or the last read didn't ask for the entire header - - stRead = (int)strlen(phop->req1) - phop->lSeek; - // how much of header left to read - - // only memcpy if request isn't out of bounds - if (stRead < 0) { - stRead = 0; - } else { - memcpy(ptr, (void*)(phop->req1 + phop->lSeek), stRead); - } - phop->lSeek += (long) stRead; // increment lSeek to new position - - // Don't count header in bytes transferred. - // Otherwise the GUI will show e.g. "400 out of 300 bytes xferred" - //phop->bytes_xferred += (double)(stRead); - daily_xfer_history.add(stRead, true); +CURLM* HTTP_CURL::curl_init() { + curl_global_init(CURL_GLOBAL_ALL); + return curl_multi_init(); +} - // see if we're done with headers - if (phop->lSeek >= (long) strlen(phop->req1)) { - phop->bSentHeader = true; - phop->lSeek = 0; - } - return stRead; - } else { - // shouldn't happen - phop->bSentHeader = true; - phop->lSeek = 0; - } +void HTTP_CURL::curl_cleanup() { + if (curlMulti) { + curl_multi_cleanup(curlMulti); } - if (phop->fileIn) { - long lFileSeek = phop->lSeek + (long) phop->file_offset; - fseek(phop->fileIn, lFileSeek, SEEK_SET); - if (!feof(phop->fileIn)) { - stRead = (int)fread(ptr, 1, stSend, phop->fileIn); - } - phop->lSeek += (long) stRead; - phop->bytes_xferred += (double)(stRead); - daily_xfer_history.add(stRead, true); - } - phop->update_speed(); - return stRead; + curl_global_cleanup(); } -curlioerr libcurl_ioctl(CURL*, curliocmd cmd, HTTP_OP* phop) { - // reset input stream to beginning - resends header - // and restarts data back to starting point - - switch(cmd) { - case CURLIOCMD_RESTARTREAD: - phop->lSeek = 0; - phop->bytes_xferred = phop->file_offset; - phop->bSentHeader = false; - break; - default: // should never get here - return CURLIOE_UNKNOWNCMD; +std::string HTTP_CURL::build_user_agent_string() { + std::stringstream ss; + ss << "BOINC client (" << HOSTTYPE << " " + << BOINC_MAJOR_VERSION << "." + << BOINC_MINOR_VERSION << "." + << BOINC_RELEASE << ")"; + if (gstate.client_brand[0]) { + ss << " (" << gstate.client_brand << ")"; } - return CURLIOE_OK; + return ss.str(); } -void libcurl_logdebug( - HTTP_OP* phop, const char* desc, char *data -) { - if (!log_flags.http_debug) return; - - char hdr[256]; - char buf[2048], *p = buf; +std::string HTTP_CURL::get_user_agent_string() { + if (user_agent_string.size() > 0) { + return user_agent_string; + } + return build_user_agent_string(); +} - snprintf(hdr, sizeof(hdr), "[ID#%u] %s", phop->trace_id, desc); +unsigned int HTTP_CURL::get_next_trace_id() { + return ++trace_count; +} - strlcpy(buf, data, sizeof(buf)); +CURLM* HTTP_CURL::get_curl_multi_handle() { + return curlMulti; +} - p = strtok(buf, "\n"); - while(p) { - msg_printf(phop->project, MSG_INFO, - "[http] %s %s\n", hdr, p - ); - p = strtok(NULL, "\n"); - } +void HTTP_CURL::set_use_http_1_0() { + use_http_1_0 = true; } -int libcurl_debugfunction( - CURL*, curl_infotype type, char *data, size_t /*size*/, HTTP_OP* phop -) { - const char* desc = NULL; - switch (type) { - case CURLINFO_TEXT: - desc = "Info: "; - break; - case CURLINFO_HEADER_OUT: - desc = "Sent header to server:"; - break; - case CURLINFO_HEADER_IN: - desc = "Received header from server:"; - break; - default: /* in case a new one is introduced to shock us */ - return 0; - } - libcurl_logdebug(phop, desc, data); - return 0; +bool HTTP_CURL::get_use_http_1_0() { + return use_http_1_0; } void HTTP_OP::init(PROJECT* p) { @@ -249,7 +152,7 @@ HTTP_OP::HTTP_OP() { http_op_state = HTTP_STATE_IDLE; http_op_type = HTTP_OP_NONE; http_op_retval = 0; - trace_id = g_trace_count++; + trace_id = HTTP_CURL::instance().get_next_trace_id(); pcurlList = NULL; // these have to be NULL, just in constructor curlEasy = NULL; pcurlFormStart = NULL; @@ -416,10 +319,6 @@ int HTTP_OP::libcurl_exec( char buf[256]; static int outfile_seqno=0; - if (g_user_agent_string[0] == 0x00) { - get_user_agent_string(); - } - if (in) { safe_strcpy(infile, in); } @@ -493,7 +392,8 @@ int HTTP_OP::libcurl_exec( // set the user agent as this boinc client & version // - curl_easy_setopt(curlEasy, CURLOPT_USERAGENT, g_user_agent_string); + curl_easy_setopt(curlEasy, CURLOPT_USERAGENT, + HTTP_CURL::instance().get_user_agent_string().c_str()); // bypass any signal handlers that curl may want to install // @@ -519,7 +419,8 @@ int HTTP_OP::libcurl_exec( // force curl to use HTTP/1.0 if config specifies it // (curl uses 1.1 by default) // - if (cc_config.http_1_0 || (cc_config.force_auth == "ntlm") || got_expectation_failed) { + if (cc_config.http_1_0 || (cc_config.force_auth == "ntlm") || + HTTP_CURL::instance().get_use_http_1_0()) { curl_easy_setopt(curlEasy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); } curl_easy_setopt(curlEasy, CURLOPT_MAXREDIRS, 50L); @@ -593,7 +494,23 @@ int HTTP_OP::libcurl_exec( // we can make the libcurl_write "fancier" in the future, // for now it just fwrite's to the file request, which is sufficient // - curl_easy_setopt(curlEasy, CURLOPT_WRITEFUNCTION, libcurl_write); + curl_easy_setopt(curlEasy, CURLOPT_WRITEFUNCTION, + [](void *ptr, size_t size, size_t nmemb, HTTP_OP* phop) -> size_t { + // take the stream param as a FILE* and write to disk + // TODO: maybe assert stRead == size*nmemb, + // add exception handling on phop members + // + size_t stWrite = fwrite(ptr, size, nmemb, phop->fileOut); + if (log_flags.http_xfer_debug) { + msg_printf(NULL, MSG_INFO, + "[http_xfer] [ID#%d] HTTP: wrote %d bytes", + phop->trace_id, static_cast(stWrite)); + } + phop->bytes_xferred += static_cast(stWrite); + phop->update_speed(); // this should update the transfer speed + daily_xfer_history.add(stWrite, false); + return stWrite; + }); // note that in my lib_write I'm sending in a pointer // to this instance of HTTP_OP // @@ -661,14 +578,93 @@ int HTTP_OP::libcurl_exec( // curl_easy_setopt(curlEasy, CURLOPT_POSTFIELDS, NULL); curl_easy_setopt(curlEasy, CURLOPT_POSTFIELDSIZE_LARGE, fs); - curl_easy_setopt(curlEasy, CURLOPT_READFUNCTION, libcurl_read); + curl_easy_setopt(curlEasy, CURLOPT_READFUNCTION, + [](void *ptr, size_t size, size_t nmemb, HTTP_OP* phop) -> size_t { + // read data from inFile (or from header if present) + // and move to buffer for Curl to send it. + // + size_t stSend = size * nmemb; + int stRead = 0; + + if (phop->req1 && !phop->bSentHeader) { + // need to send headers first, then data file + // so requests from 0 to strlen(req1)-1 are from memory, + // and from strlen(req1) to content_length are from the file + if (phop->lSeek < static_cast(strlen(phop->req1))) { + // need to read header, either just starting to read + // (i.e. this is the first time in this function + // for this phop) + // or the last read didn't ask for the entire header + + stRead = static_cast(strlen(phop->req1)) - phop->lSeek; + // how much of header left to read + + // only memcpy if request isn't out of bounds + if (stRead < 0) { + stRead = 0; + } else { + memcpy(ptr, + reinterpret_cast(phop->req1 + phop->lSeek), + stRead); + } + // increment lSeek to new position + phop->lSeek += static_cast(stRead); + + // Don't count header in bytes transferred. + // Otherwise the GUI will show + // e.g. "400 out of 300 bytes xferred" + //phop->bytes_xferred += (double)(stRead); + daily_xfer_history.add(stRead, true); + + // see if we're done with headers + if (phop->lSeek >= static_cast(strlen(phop->req1))) { + phop->bSentHeader = true; + phop->lSeek = 0; + } + return stRead; + } else { + // shouldn't happen + phop->bSentHeader = true; + phop->lSeek = 0; + } + } + if (phop->fileIn) { + long lFileSeek = phop->lSeek + + static_cast(phop->file_offset); + fseek(phop->fileIn, lFileSeek, SEEK_SET); + if (!feof(phop->fileIn)) { + stRead = + static_cast(fread(ptr, 1, stSend, phop->fileIn)); + } + phop->lSeek += static_cast(stRead); + phop->bytes_xferred += static_cast(stRead); + daily_xfer_history.add(stRead, true); + } + phop->update_speed(); + return stRead; + }); // in my lib_write I'm sending in a pointer to this instance of HTTP_OP // curl_easy_setopt(curlEasy, CURLOPT_READDATA, this); // callback function to rewind input file // - curl_easy_setopt(curlEasy, CURLOPT_IOCTLFUNCTION, libcurl_ioctl); + curl_easy_setopt(curlEasy, CURLOPT_IOCTLFUNCTION, + [](CURL*, curliocmd cmd, HTTP_OP* phop) -> curlioerr { + // reset input stream to beginning - resends header + // and restarts data back to starting point + + switch(cmd) { + case CURLIOCMD_RESTARTREAD: + phop->lSeek = 0; + phop->bytes_xferred = phop->file_offset; + phop->bSentHeader = false; + break; + default: // should never get here + return CURLIOE_UNKNOWNCMD; + } + return CURLIOE_OK; + }); curl_easy_setopt(curlEasy, CURLOPT_IOCTLDATA, this); curl_easy_setopt(curlEasy, CURLOPT_POST, 1L); @@ -698,14 +694,45 @@ int HTTP_OP::libcurl_exec( // turn on debug info if tracing enabled // if (log_flags.http_debug) { - curl_easy_setopt(curlEasy, CURLOPT_DEBUGFUNCTION, libcurl_debugfunction); - curl_easy_setopt(curlEasy, CURLOPT_DEBUGDATA, this ); + curl_easy_setopt(curlEasy, CURLOPT_DEBUGFUNCTION, + [](CURL*, curl_infotype type, char *data, size_t, + HTTP_OP* phop) -> int { + if (!log_flags.http_debug) return 0; + + const char* desc = NULL; + switch (type) { + case CURLINFO_TEXT: + desc = "Info: "; + break; + case CURLINFO_HEADER_OUT: + desc = "Sent header to server:"; + break; + case CURLINFO_HEADER_IN: + desc = "Received header from server:"; + break; + default: /* in case a new one is introduced to shock us */ + return 0; + } + + std::stringstream ssData(data); + std::string line; + while(std::getline(ssData, line)) { + msg_printf(phop->project, MSG_INFO, + "[http] [ID#%u] %s %s\n", phop->trace_id, desc, + line.c_str() + ); + } + return 0; + }); + + curl_easy_setopt(curlEasy, CURLOPT_DEBUGDATA, this); curl_easy_setopt(curlEasy, CURLOPT_VERBOSE, 1L); } // last but not least, add this to the curl_multi - curlMErr = curl_multi_add_handle(g_curlMulti, curlEasy); + curlMErr = curl_multi_add_handle( + HTTP_CURL::instance().get_curl_multi_handle(), curlEasy); if (curlMErr != CURLM_OK && curlMErr != CURLM_CALL_MULTI_PERFORM) { // bad error, couldn't attach easy curl handle msg_printf(0, MSG_INTERNAL_ERROR, @@ -855,27 +882,6 @@ void HTTP_OP::setup_proxy_session(bool no_proxy) { } } - -// the file descriptor sets need to be global so libcurl has access always -// -fd_set read_fds, write_fds, error_fds; - -// call these once at the start of the program and once at the end -// -int curl_init() { - curl_global_init(CURL_GLOBAL_ALL); - g_curlMulti = curl_multi_init(); - return (int)(g_curlMulti == NULL); -} - -int curl_cleanup() { - if (g_curlMulti) { - curl_multi_cleanup(g_curlMulti); - } - curl_global_cleanup(); - return 0; -} - void HTTP_OP::close_socket() { // this cleans up the curlEasy, and "spoofs" the old close_socket // @@ -888,8 +894,9 @@ void HTTP_OP::close_socket() { curl_formfree(pcurlFormEnd); pcurlFormStart = pcurlFormEnd = NULL; } - if (curlEasy && g_curlMulti) { // release this handle - curl_multi_remove_handle(g_curlMulti, curlEasy); + CURLM *curlMulti = HTTP_CURL::instance().get_curl_multi_handle(); + if (curlEasy && curlMulti) { // release this handle + curl_multi_remove_handle(curlMulti, curlEasy); curl_easy_cleanup(curlEasy); curlEasy = NULL; } @@ -910,7 +917,8 @@ void HTTP_OP::close_files() { void HTTP_OP_SET::get_fdset(FDSET_GROUP& fg) { curl_multi_fdset( - g_curlMulti, &fg.read_fds, &fg.write_fds, &fg.exc_fds, &fg.max_fd + HTTP_CURL::instance().get_curl_multi_handle(), + &fg.read_fds, &fg.write_fds, &fg.exc_fds, &fg.max_fd ); } @@ -981,7 +989,7 @@ void HTTP_OP::handle_messages(CURLMsg *pcurlMsg) { break; default: // 400 if (response == HTTP_STATUS_EXPECTATION_FAILED) { - got_expectation_failed = true; + HTTP_CURL::instance().set_use_http_1_0(); } http_op_retval = ERR_HTTP_PERMANENT; safe_strcpy(error_msg, boincerror(response)); @@ -1062,7 +1070,7 @@ void HTTP_OP_SET::got_select(FDSET_GROUP&, double timeout) { int iNumMsg; HTTP_OP* hop = NULL; CURLMsg *pcurlMsg = NULL; - + CURLM *curlMulti = HTTP_CURL::instance().get_curl_multi_handle(); int iRunning = 0; // curl flags for max # of fds & # running queries CURLMcode curlMErr; @@ -1070,7 +1078,7 @@ void HTTP_OP_SET::got_select(FDSET_GROUP&, double timeout) { // use timeout value so that we don't hog CPU in this loop // while (1) { - curlMErr = curl_multi_perform(g_curlMulti, &iRunning); + curlMErr = curl_multi_perform(curlMulti, &iRunning); if (curlMErr != CURLM_CALL_MULTI_PERFORM) break; if (dtime() - gstate.now > timeout) break; } @@ -1078,7 +1086,7 @@ void HTTP_OP_SET::got_select(FDSET_GROUP&, double timeout) { // read messages from curl that may have come in from the above loop // while (1) { - pcurlMsg = curl_multi_info_read(g_curlMulti, &iNumMsg); + pcurlMsg = curl_multi_info_read(curlMulti, &iNumMsg); if (!pcurlMsg) break; // if we have a msg, then somebody finished diff --git a/client/http_curl.h b/client/http_curl.h index de4227265fe..7e56995ba8b 100644 --- a/client/http_curl.h +++ b/client/http_curl.h @@ -1,6 +1,6 @@ // This file is part of BOINC. -// http://boinc.berkeley.edu -// Copyright (C) 2008 University of California +// https://boinc.berkeley.edu +// Copyright (C) 2025 University of California // // BOINC is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License @@ -24,14 +24,13 @@ #ifndef BOINC_HTTP_CURL_H #define BOINC_HTTP_CURL_H +#include + #include #include "network.h" #include "proxy_info.h" -extern int curl_init(); -extern int curl_cleanup(); - #define HTTP_OP_NONE 0 #define HTTP_OP_GET 1 // data sink is a file (used for file download) @@ -201,4 +200,35 @@ class HTTP_OP_SET { }; +class HTTP_CURL { +public: + std::string get_user_agent_string(); + CURLM* get_curl_multi_handle(); + unsigned int get_next_trace_id(); + void set_use_http_1_0(); + bool get_use_http_1_0(); + + static HTTP_CURL& instance() { + static HTTP_CURL instance; + return instance; + } + HTTP_CURL(HTTP_CURL const&) = delete; + void operator=(HTTP_CURL const&) = delete; +private: + HTTP_CURL(); + ~HTTP_CURL(); + + CURLM* curl_init(); + void curl_cleanup(); + static std::string build_user_agent_string(); + + CURLM* curlMulti; + std::string user_agent_string; + std::atomic_uint trace_count; + // Whether we've got a 417 HTTP error. + // If we did, it's probably because we talked HTTP 1.1 to a 1.0 proxy; + // use 1.0 from now on. + std::atomic_bool use_http_1_0; +}; + #endif // BOINC_HTTP_CURL_H diff --git a/client/main.cpp b/client/main.cpp index 62e3c6c24b7..7963f286cd2 100644 --- a/client/main.cpp +++ b/client/main.cpp @@ -363,8 +363,6 @@ static int initialize() { } #endif - curl_init(); - #ifdef _WIN32 if(!startup_idle_monitor()) { log_message_error( @@ -397,8 +395,6 @@ static int finalize() { #endif - curl_cleanup(); - #ifdef _DEBUG gstate.free_mem(); #endif diff --git a/tests/executeUnitTests.sh b/tests/executeUnitTests.sh index 837c514996a..52b159e2f67 100755 --- a/tests/executeUnitTests.sh +++ b/tests/executeUnitTests.sh @@ -76,9 +76,9 @@ make if [ $? -ne 0 ]; then cd ../..; exit 1; fi if [[ "$OSTYPE" == "darwin"* ]]; then - MODULES="lib" + MODULES="lib client" else - MODULES="lib sched" + MODULES="lib client sched" fi for T in ${MODULES}; do XML_FLAGS="" diff --git a/tests/unit-tests/CMakeLists.txt b/tests/unit-tests/CMakeLists.txt index f659706c029..38e46947e68 100644 --- a/tests/unit-tests/CMakeLists.txt +++ b/tests/unit-tests/CMakeLists.txt @@ -22,14 +22,22 @@ project(BOINC_unit_testing) enable_testing() set (CMAKE_CXX_STANDARD 17) -set (CMAKE_CXX_FLAGS "-g -Wall -Wextra -Werror --coverage") +#set (CMAKE_CXX_FLAGS "-g -Wall -Wextra -Werror --coverage") +set (CMAKE_CXX_FLAGS "-g -Wall -Wextra --coverage") if (APPLE) find_package(GTest REQUIRED PATHS ${PROJECT_SOURCE_DIR}/../../3rdParty/osx/vcpkg/installed/arm64-osx/share NO_DEFAULT_PATH) find_package(libzip REQUIRED PATHS ${PROJECT_SOURCE_DIR}/../../3rdParty/osx/vcpkg/installed/arm64-osx/share NO_DEFAULT_PATH) + find_package(CURL REQUIRED PATHS ${PROJECT_SOURCE_DIR}/../../3rdParty/osx/vcpkg/installed/arm64-osx/share NO_DEFAULT_PATH) + find_package(ZLIB REQUIRED PATHS ${PROJECT_SOURCE_DIR}/../../3rdParty/osx/vcpkg/installed/arm64-osx/share NO_DEFAULT_PATH) + find_package(OpenSSL REQUIRED PATHS ${PROJECT_SOURCE_DIR}/../../3rdParty/osx/vcpkg/installed/arm64-osx/share NO_DEFAULT_PATH) else() - find_package(Threads REQUIRED) find_package(GTest REQUIRED) + find_package(ZLIB REQUIRED) + find_package(CURL REQUIRED) + find_package(OpenSSL REQUIRED) + find_package(Threads REQUIRED) + find_package(X11 REQUIRED) find_package(PkgConfig REQUIRED) pkg_check_modules(LIBZIP REQUIRED IMPORTED_TARGET libzip) endif() @@ -38,6 +46,7 @@ endif() # so the paths below are hardcoded and are mainly suited for building on Travis CI # TODO: make paths configurable and generate them from boinc make or switch boinc to cmake +include_directories("${PROJECT_SOURCE_DIR}/../../client") include_directories("${PROJECT_SOURCE_DIR}/../../lib") include_directories("${PROJECT_SOURCE_DIR}/../../zip") if (APPLE) @@ -65,6 +74,7 @@ else() message(STATUS "mysql_lib: ${MYSQL_LIB}") endif() +add_subdirectory(client) add_subdirectory(lib) if (NOT APPLE) add_subdirectory(sched) diff --git a/tests/unit-tests/client/CMakeLists.txt b/tests/unit-tests/client/CMakeLists.txt new file mode 100644 index 00000000000..ac0da6c5ba1 --- /dev/null +++ b/tests/unit-tests/client/CMakeLists.txt @@ -0,0 +1,107 @@ +# This file is part of BOINC. +# https://boinc.berkeley.edu +# Copyright (C) 2025 University of California +# +# BOINC is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License +# as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# BOINC is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with BOINC. If not, see . +# + +file(GLOB SRCS *.cpp) + +list(APPEND SRCS + ../../../client/acct_mgr.cpp + ../../../client/acct_setup.cpp + ../../../client/app.cpp + ../../../client/app_config.cpp + ../../../client/app_control.cpp + ../../../client/app_start.cpp + ../../../client/app_test.cpp + ../../../client/async_file.cpp + ../../../client/client_msgs.cpp + ../../../client/client_state.cpp + ../../../client/client_types.cpp + ../../../client/coproc_sched.cpp + ../../../client/cpu_sched.cpp + ../../../client/cs_account.cpp + ../../../client/cs_apps.cpp + ../../../client/cs_benchmark.cpp + ../../../client/cs_cmdline.cpp + ../../../client/cs_files.cpp + ../../../client/cs_notice.cpp + ../../../client/cs_platforms.cpp + ../../../client/cs_prefs.cpp + ../../../client/cs_proxy.cpp + ../../../client/cs_scheduler.cpp + ../../../client/cs_sporadic.cpp + ../../../client/cs_statefile.cpp + ../../../client/cs_trickle.cpp + ../../../client/current_version.cpp + ../../../client/dhrystone.cpp + ../../../client/dhrystone2.cpp + ../../../client/file_names.cpp + ../../../client/file_xfer.cpp + ../../../client/gpu_amd.cpp + ../../../client/gpu_detect.cpp + ../../../client/gpu_nvidia.cpp + ../../../client/gpu_opencl.cpp + ../../../client/gui_http.cpp + ../../../client/gui_rpc_server.cpp + ../../../client/gui_rpc_server_ops.cpp + ../../../client/hostinfo_network.cpp + ../../../client/http_curl.cpp + ../../../client/log_flags.cpp + ../../../client/mac_address.cpp + ../../../client/net_stats.cpp + ../../../client/pers_file_xfer.cpp + ../../../client/project.cpp + ../../../client/project_list.cpp + ../../../client/result.cpp + ../../../client/rr_sim.cpp + ../../../client/sandbox.cpp + ../../../client/scheduler_op.cpp + ../../../client/thread.cpp + ../../../client/time_stats.cpp + ../../../client/whetstone.cpp + ../../../client/work_fetch.cpp +) + +set (SRCS_UNIX + ../../../client/hostinfo_linux.cpp + ../../../client/hostinfo_unix.cpp +) + +set (SRCS_WINDOWS + ../../../client/hostinfo_win.cpp +) + +if (WIN32) + list(APPEND SRCS ${SRCS_WINDOWS}) +else () + list(APPEND SRCS ${SRCS_UNIX}) +endif () + +add_executable(test_client + ${SRCS} +) + +set (X11_LIBS "") +if (UNIX AND NOT APPLE) + list(APPEND X11_LIBS + X11::X11 + X11::Xss + ) +endif () + +TARGET_LINK_LIBRARIES(test_client "${BOINC_CRYPT_LIB}" "${BOINC_LIB}" "${BOINC_ZIP_LIB}" pthread GTest::gtest GTest::gtest_main CURL::libcurl ZLIB::ZLIB OpenSSL::SSL OpenSSL::Crypto ${X11_LIBS}) + +add_test(NAME test_client COMMAND test_client) diff --git a/tests/unit-tests/client/test_http_curl.cpp b/tests/unit-tests/client/test_http_curl.cpp new file mode 100644 index 00000000000..454179594a4 --- /dev/null +++ b/tests/unit-tests/client/test_http_curl.cpp @@ -0,0 +1,66 @@ +// This file is part of BOINC. +// https://boinc.berkeley.edu +// Copyright (C) 2025 University of California +// +// BOINC is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// BOINC is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with BOINC. If not, see . + +#include +#include + +#include "gtest/gtest.h" +#include "http_curl.h" + +using namespace std; + +namespace test_http_curl { + class test_http_curl : public ::testing::Test {}; + + TEST_F(test_http_curl, test_user_agent_string) { + ASSERT_NE(string(""), HTTP_CURL::instance().get_user_agent_string()); + ASSERT_EQ(HTTP_CURL::instance().get_user_agent_string(), HTTP_CURL::instance().get_user_agent_string()); + } + + TEST_F(test_http_curl, test_curl_multi_handle) { + ASSERT_NE(nullptr, HTTP_CURL::instance().get_curl_multi_handle()); + ASSERT_EQ(HTTP_CURL::instance().get_curl_multi_handle(), HTTP_CURL::instance().get_curl_multi_handle()); + } + + TEST_F(test_http_curl, test_http_1_0_flag) { + // default is false + ASSERT_EQ(false, HTTP_CURL::instance().get_use_http_1_0()); + HTTP_CURL::instance().set_use_http_1_0(); + ASSERT_EQ(true, HTTP_CURL::instance().get_use_http_1_0()); + } + + TEST_F(test_http_curl, test_atomic_trace_count) { + constexpr auto num_threads = 10u; + constexpr auto calls_per_thread = 100'000u; + vector threads; + threads.reserve(num_threads); + const auto start_trace_id = HTTP_CURL::instance().get_next_trace_id(); + for (auto i = 0u; i < num_threads; ++i) { + threads.emplace_back([]() { + for (auto j = 0u; j < calls_per_thread; ++j) { + HTTP_CURL::instance().get_next_trace_id(); + } + }); + } + for (auto& t : threads) { + t.join(); + } + constexpr auto expected = num_threads * calls_per_thread; + const auto actual = HTTP_CURL::instance().get_next_trace_id(); + EXPECT_EQ(expected + start_trace_id + 1, actual); + } +}