From 9670a99712ad39339645816af7c2776ccfe678f2 Mon Sep 17 00:00:00 2001 From: Justin Hiemstra Date: Tue, 16 Apr 2024 18:42:10 +0000 Subject: [PATCH 1/5] Add very basic unit tests for S3 and HTTP This sets up a single unit test for both the S3 and HTTP plugins. Now that the framework is in place, any contributions to the plugins should include unit tests for the new work (and if the developer is generous, a unit test or two for something that already exists -- we've got quite the backlog!) --- CMakeLists.txt | 16 +++++++ README.md | 111 ++++++++++++++++---------------------------- src/HTTPCommands.cc | 15 ------ src/S3Commands.cc | 13 ++++-- test/CMakeLists.txt | 51 ++++++++++++++++++++ test/http_tests.cc | 52 +++++++++++++++++++++ test/s3_tests.cc | 65 ++++++++++++++++++++++++++ 7 files changed, 233 insertions(+), 90 deletions(-) create mode 100644 test/CMakeLists.txt create mode 100644 test/http_tests.cc create mode 100644 test/s3_tests.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 9cecd44..b29f2e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,9 @@ cmake_minimum_required( VERSION 3.13 ) project( xrootd-http/s3 ) +option( XROOTD_PLUGINS_BUILD_UNITTESTS "Build the scitokens-cpp unit tests" OFF ) +option( XROOTD_PLUGINS_EXTERNAL_GTEST "Use an external/pre-installed copy of GTest" OFF ) + set( CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake ) set( CMAKE_BUILD_TYPE Debug ) @@ -73,6 +76,19 @@ install( LIBRARY DESTINATION ${LIB_INSTALL_DIR} ) +if( XROOTD_PLUGINS_BUILD_UNITTESTS ) + if( NOT XROOTD_PLUGINS_EXTERNAL_GTEST ) +include(ExternalProject) +ExternalProject_Add(gtest + PREFIX external/gtest + URL ${CMAKE_CURRENT_SOURCE_DIR}/vendor/gtest + INSTALL_COMMAND : +) +endif() +enable_testing() +add_subdirectory(test) +endif() + #install( # FILES ${CMAKE_SOURCE_DIR}/configs/60-s3.cfg # DESTINATION ${CMAKE_INSTALL_PREFIX}/etc/xrootd/config.d/ diff --git a/README.md b/README.md index 13ed37f..c4ff82c 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,46 @@ -S3/HTTP filesystem plugins for XRootD -================================ +# S3/HTTP filesystem plugins for XRootD +These filesystem plugins for [XRootD](https://github.com/xrootd/xrootd) allow you to serve objects from S3 and HTTP backends through an XRootD server. -These filesystem plugins are intended to demonstrate the ability to have an S3 bucket -or raw HTTP server as an underlying "filesystem" for an XRootD server. - -They are currently quite experimental, aiming to show a "minimum viable product". - -Building and Installing ------------------------ +## Building and Installing Assuming XRootD, CMake>=3.13 and gcc>=8 are already installed, run: ``` mkdir build cd build cmake .. -make install -``` +make +# For system installation, uncomment: +# make install +``` If building XRootD from source instead, add `-DXROOTD_DIR` to the CMake command line to point it at the installed directory. -Configuration -------------- +### Building with Tests + +Unit tests for this repository require `gtest`, which for RHEL-based linux distributions can be installed with: +```bash +dnf install gtest +``` + +Once `gtest` is installed, the unit tests can be compiled with a slight modification to your build command: + +``` +mkdir build +cd build +cmake -DXROOTD_PLUGINS_BUILD_UNITTESTS=ON -DXROOTD_PLUGINS_EXTERNAL_GTEST=ON .. +make +``` -## Configure an HTTP Server Backend +This creates the directory `build/test` with two unit test executables that can be run: +- `build/test/s3-gtest` +- `build/test/http-gtest` + +## Configuration + +### Configure an HTTP Server Backend To configure the HTTP server plugin, add the following line to the Xrootd configuration file: @@ -36,29 +51,9 @@ ofs.osslib Here's a minimal config file ``` -########################################################################### -# This is a very simple sample configuration file sufficient to start an # -# xrootd file caching proxy server using the default port 1094. This # -# server runs by itself (stand-alone) and does not assume it is part of a # -# cluster. You can then connect to this server to access files in '/tmp'. # -# Consult the the reference manuals on how to create more complicated # -# configurations. # -# # -# On successful start-up you will see 'initialization completed' in the # -# last message. You can now connect to the xrootd server. # -# # -# Note: You should always create a *single* configuration file for all # -# daemons related to xrootd. # -########################################################################### - -# The adminpath and pidpath variables indicate where the pid and various -# IPC files should be placed. These can be placed in /tmp for a developer- -# quality server. -# -all.adminpath /var/spool/xrootd -all.pidpath /run/xrootd - -# Enable the HTTP protocol on port 1094 (same as the default XRootD port): +# Enable the HTTP protocol on port 1094 (same as the default XRootD port) +# NOTE: This is NOT the HTTP plugin -- it is the library XRootD uses to +# speak the HTTP protocol, as opposed to the root protocol, for incoming requests xrd.protocol http:1094 libXrdHttp.so # Allow access to path with given prefix. @@ -73,15 +68,12 @@ ofs.osslib libXrdHTTPServer.so # Upon last testing, the plugin did not yet work in async mode xrootd.async off - - # Configure the upstream HTTP server that XRootD is to treat as a filesystem httpserver.host_name httpserver.host_url ``` - -## Configure an S3 Backend +### Configure an S3 Backend To configure the S3 plugin, add the following line to the Xrootd configuration file: @@ -92,29 +84,8 @@ ofs.osslib Here's a minimal config file ``` -########################################################################### -# This is a very simple sample configuration file sufficient to start an # -# xrootd file caching proxy server using the default port 1094. This # -# server runs by itself (stand-alone) and does not assume it is part of a # -# cluster. You can then connect to this server to access files in '/tmp'. # -# Consult the the reference manuals on how to create more complicated # -# configurations. # -# # -# On successful start-up you will see 'initialization completed' in the # -# last message. You can now connect to the xrootd server. # -# # -# Note: You should always create a *single* configuration file for all # -# daemons related to xrootd. # -########################################################################### - -# The adminpath and pidpath variables indicate where the pid and various -# IPC files should be placed. These can be placed in /tmp for a developer- -# quality server. -# -all.adminpath /var/spool/xrootd -all.pidpath /run/xrootd - -# Enable the HTTP protocol on port 1094 (same as the default XRootD port): +# Enable the HTTP protocol on port 1094 (same as the default XRootD port) +# The S3 plugin use xrd.protocol http:1094 libXrdHttp.so # Allow access to path with given prefix. @@ -161,10 +132,9 @@ s3.url_style virtual ``` -Startup and Testing -------------------- +## Startup and Testing -## HTTP Server Backend +### HTTP Server Backend Assuming you named the config file `xrootd-http.cfg`, as a non-rootly user run: @@ -178,11 +148,8 @@ In a separate terminal, run curl -v http://localhost:1094// ``` -## S3 Server Backend +### S3 Server Backend Startup and Testing -------------------- - -## HTTP Server Backend Assuming you named the config file `xrootd-s3.cfg`, as a non-rootly user run: @@ -193,5 +160,5 @@ xrootd -d -c xrootd-s3.cfg In a separate terminal, run ``` -curl -v http://localhost:1094/// +curl -v http://localhost:1094// ``` \ No newline at end of file diff --git a/src/HTTPCommands.cc b/src/HTTPCommands.cc index 6579180..361340e 100644 --- a/src/HTTPCommands.cc +++ b/src/HTTPCommands.cc @@ -81,21 +81,6 @@ bool HTTPRequest::parseProtocol(const std::string &url, std::string &protocol) { } protocol = substring(url, 0, i); - // This func used to parse the entire URL according - // to the Amazon canonicalURI specs, but that functionality - // has since been moved to the Amazon subclass. Now it just - // grabs the protocol. Leaving the old stuff commented for - // now, just in case... - - // auto j = url.find( "/", i + 3 ); - // if( j == std::string::npos ) { - // host = substring( url, i + 3 ); - // path = "/"; - // return true; - // } - - // host = substring( url, i + 3, j ); - // path = substring( url, j ); return true; } diff --git a/src/S3Commands.cc b/src/S3Commands.cc index f5883dc..dc12889 100644 --- a/src/S3Commands.cc +++ b/src/S3Commands.cc @@ -55,8 +55,8 @@ std::string AmazonRequest::canonicalizeQueryString() { } // Takes in the configured `s3.service_url` and uses the bucket/object requested -// to generate the virtual host URL, as well as the canonical URI (which is the -// path to the object). +// to generate the host URL, as well as the canonical URI (which is the path to +// the object). bool AmazonRequest::parseURL(const std::string &url, std::string &path) { auto i = url.find("://"); if (i == std::string::npos) { @@ -71,7 +71,14 @@ bool AmazonRequest::parseURL(const std::string &url, std::string &path) { // :// and the last / host = substring(url, i + 3); // Likewise, the path is going to be /bucket/object - path = "/" + bucket + "/" + object; + // Sometimes we intentionally configure the plugin with no bucket because we + // assume the incoming object request already encodes the bucket. This is used + // for exporting many buckets from a single endpoint. + if (bucket.empty()) { + path = "/" + object; + } else { + path = "/" + bucket + "/" + object; + } } else { // In virtual-style requests, the host should be determined as // everything between diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..066b3e6 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,51 @@ +add_executable( s3-gtest s3_tests.cc + ../src/AWSv4-impl.cc + ../src/logging.cc + ../src/S3AccessInfo.cc + ../src/S3File.cc + ../src/S3FileSystem.cc + ../src/shortfile.cc + ../src/stl_string_utils.cc + ../src/HTTPCommands.cc + ../src/S3Commands.cc +) + +add_executable( http-gtest http_tests.cc + ../src/HTTPFile.cc + ../src/HTTPFileSystem.cc + ../src/HTTPCommands.cc + ../src/stl_string_utils.cc + ../src/shortfile.cc + ../src/logging.cc +) + + +if( NOT XROOTD_PLUGINS_EXTERNAL_GTEST ) + add_dependencies(s3-gtest gtest) + add_dependencies(http-gtest gtest) + include_directories("${PROJECT_SOURCE_DIR}/vendor/gtest/googletest/include") +endif() + +if(XROOTD_PLUGINS_EXTERNAL_GTEST) + set(LIBGTEST "gtest") +else() + set(LIBGTEST "${CMAKE_BINARY_DIR}/external/gtest/src/gtest-build/lib/libgtest.a") +endif() + +target_link_libraries(s3-gtest XrdHTTPServer XrdS3 "${LIBGTEST}" pthread) +target_link_libraries(http-gtest XrdHTTPServer "${LIBGTEST}" pthread) + + +add_test( + NAME + s3-unit + COMMAND + ${CMAKE_CURRENT_BINARY_DIR}/s3-gtest +) + +add_test( + NAME + http-unit + COMMAND + ${CMAKE_CURRENT_BINARY_DIR}/http-gtest +) \ No newline at end of file diff --git a/test/http_tests.cc b/test/http_tests.cc new file mode 100644 index 0000000..9638d79 --- /dev/null +++ b/test/http_tests.cc @@ -0,0 +1,52 @@ +/*************************************************************** + * + * Copyright (C) 2024, Pelican Project, Morgridge Institute for Research + * + * Licensed under the Apache License, Version 2.0 (the "License"); you + * may not use this file except in compliance with the License. You may + * obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ***************************************************************/ + +#include "../src/HTTPCommands.hh" + +#include +#include +#include +#include + +class TestHTTPRequest : public HTTPRequest { +public: + XrdSysLogger log{}; + XrdSysError err{&log, "TestS3CommandsLog"}; + + TestHTTPRequest(const std::string& url) + : HTTPRequest(url, err) {} +}; + +TEST(TestHTTPParseProtocol, Test1) { + const std::string httpURL = "https://my-test-url.com:443"; + TestHTTPRequest req{httpURL}; + + // Test parsing of https + std::string protocol; + req.parseProtocol("https://my-test-url.com:443", protocol); + ASSERT_EQ(protocol, "https"); + + // Test parsing for http + req.parseProtocol("http://my-test-url.com:443", protocol); + ASSERT_EQ(protocol, "http"); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/test/s3_tests.cc b/test/s3_tests.cc new file mode 100644 index 0000000..d1fcd12 --- /dev/null +++ b/test/s3_tests.cc @@ -0,0 +1,65 @@ +/*************************************************************** + * + * Copyright (C) 2024, Pelican Project, Morgridge Institute for Research + * + * Licensed under the Apache License, Version 2.0 (the "License"); you + * may not use this file except in compliance with the License. You may + * obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ***************************************************************/ + +#include "../src/S3Commands.hh" + +#include +#include +#include + +class TestAmazonRequest : public AmazonRequest { +public: + XrdSysLogger log{}; + XrdSysError err{&log, "TestS3CommandsLog"}; + + TestAmazonRequest(const std::string& url, const std::string& akf, const std::string& skf, + const std::string& bucket, const std::string& object, const std::string& path, + int sigVersion) + : AmazonRequest(url, akf, skf, bucket, object, path, sigVersion, err) {} + + // For getting access to otherwise-protected members + std::string getHostUrl() const { + return hostUrl; + } +}; + +TEST(TestS3URLGeneration, Test1) { + const std::string serviceUrl = "https://s3-service.com:443"; + const std::string b = "test-bucket"; + const std::string o = "test-object"; + + // Test path-style URL generation + TestAmazonRequest pathReq{serviceUrl, "akf", "skf", b, o, "path", 4}; + std::string generatedHostUrl = pathReq.getHostUrl(); + ASSERT_EQ(generatedHostUrl, "https://s3-service.com:443/test-bucket/test-object"); + + // Test virtual-style URL generation + TestAmazonRequest virtReq{serviceUrl, "akf", "skf", b, o, "virtual", 4}; + generatedHostUrl = virtReq.getHostUrl(); + ASSERT_EQ(generatedHostUrl, "https://test-bucket.s3-service.com:443/test-object"); + + // Test path-style with empty bucket (which we use for exporting an entire endpoint) + TestAmazonRequest pathReqNoBucket{serviceUrl, "akf", "skf", "", o, "path", 4}; + generatedHostUrl = pathReqNoBucket.getHostUrl(); + ASSERT_EQ(generatedHostUrl, "https://s3-service.com:443/test-object"); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file From 98cb22e35c587e7f491e5fd75287c1bd8e7684ff Mon Sep 17 00:00:00 2001 From: Justin Hiemstra Date: Tue, 16 Apr 2024 18:58:03 +0000 Subject: [PATCH 2/5] Add missing newlines at EOF --- test/CMakeLists.txt | 2 +- test/http_tests.cc | 2 +- test/s3_tests.cc | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 066b3e6..8f5589b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -48,4 +48,4 @@ add_test( http-unit COMMAND ${CMAKE_CURRENT_BINARY_DIR}/http-gtest -) \ No newline at end of file +) diff --git a/test/http_tests.cc b/test/http_tests.cc index 9638d79..8bbd43a 100644 --- a/test/http_tests.cc +++ b/test/http_tests.cc @@ -49,4 +49,4 @@ TEST(TestHTTPParseProtocol, Test1) { int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); -} \ No newline at end of file +} diff --git a/test/s3_tests.cc b/test/s3_tests.cc index d1fcd12..5521349 100644 --- a/test/s3_tests.cc +++ b/test/s3_tests.cc @@ -62,4 +62,4 @@ TEST(TestS3URLGeneration, Test1) { int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); -} \ No newline at end of file +} From b9d7827e630d8095ef81040b0d1b11d17cc580c5 Mon Sep 17 00:00:00 2001 From: Justin Hiemstra Date: Tue, 16 Apr 2024 19:05:58 +0000 Subject: [PATCH 3/5] Remove unused import --- test/http_tests.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/test/http_tests.cc b/test/http_tests.cc index 8bbd43a..a333d7d 100644 --- a/test/http_tests.cc +++ b/test/http_tests.cc @@ -19,7 +19,6 @@ #include "../src/HTTPCommands.hh" #include -#include #include #include From c0939f4ec7290e7f6fa7f54e9e99ef1d17b2f5ed Mon Sep 17 00:00:00 2001 From: Justin Hiemstra Date: Tue, 16 Apr 2024 19:09:19 +0000 Subject: [PATCH 4/5] Run clang-format on new files --- test/http_tests.cc | 33 ++++++++++++------------- test/s3_tests.cc | 61 ++++++++++++++++++++++++---------------------- 2 files changed, 48 insertions(+), 46 deletions(-) diff --git a/test/http_tests.cc b/test/http_tests.cc index a333d7d..49a21c0 100644 --- a/test/http_tests.cc +++ b/test/http_tests.cc @@ -18,34 +18,33 @@ #include "../src/HTTPCommands.hh" -#include #include #include +#include class TestHTTPRequest : public HTTPRequest { -public: - XrdSysLogger log{}; - XrdSysError err{&log, "TestS3CommandsLog"}; + public: + XrdSysLogger log{}; + XrdSysError err{&log, "TestS3CommandsLog"}; - TestHTTPRequest(const std::string& url) - : HTTPRequest(url, err) {} + TestHTTPRequest(const std::string &url) : HTTPRequest(url, err) {} }; TEST(TestHTTPParseProtocol, Test1) { - const std::string httpURL = "https://my-test-url.com:443"; - TestHTTPRequest req{httpURL}; + const std::string httpURL = "https://my-test-url.com:443"; + TestHTTPRequest req{httpURL}; - // Test parsing of https - std::string protocol; - req.parseProtocol("https://my-test-url.com:443", protocol); - ASSERT_EQ(protocol, "https"); + // Test parsing of https + std::string protocol; + req.parseProtocol("https://my-test-url.com:443", protocol); + ASSERT_EQ(protocol, "https"); - // Test parsing for http - req.parseProtocol("http://my-test-url.com:443", protocol); - ASSERT_EQ(protocol, "http"); + // Test parsing for http + req.parseProtocol("http://my-test-url.com:443", protocol); + ASSERT_EQ(protocol, "http"); } int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); } diff --git a/test/s3_tests.cc b/test/s3_tests.cc index 5521349..8b9fdc6 100644 --- a/test/s3_tests.cc +++ b/test/s3_tests.cc @@ -18,48 +18,51 @@ #include "../src/S3Commands.hh" -#include #include #include +#include class TestAmazonRequest : public AmazonRequest { -public: - XrdSysLogger log{}; - XrdSysError err{&log, "TestS3CommandsLog"}; + public: + XrdSysLogger log{}; + XrdSysError err{&log, "TestS3CommandsLog"}; - TestAmazonRequest(const std::string& url, const std::string& akf, const std::string& skf, - const std::string& bucket, const std::string& object, const std::string& path, - int sigVersion) - : AmazonRequest(url, akf, skf, bucket, object, path, sigVersion, err) {} + TestAmazonRequest(const std::string &url, const std::string &akf, + const std::string &skf, const std::string &bucket, + const std::string &object, const std::string &path, + int sigVersion) + : AmazonRequest(url, akf, skf, bucket, object, path, sigVersion, err) {} - // For getting access to otherwise-protected members - std::string getHostUrl() const { - return hostUrl; - } + // For getting access to otherwise-protected members + std::string getHostUrl() const { return hostUrl; } }; TEST(TestS3URLGeneration, Test1) { - const std::string serviceUrl = "https://s3-service.com:443"; - const std::string b = "test-bucket"; - const std::string o = "test-object"; + const std::string serviceUrl = "https://s3-service.com:443"; + const std::string b = "test-bucket"; + const std::string o = "test-object"; - // Test path-style URL generation - TestAmazonRequest pathReq{serviceUrl, "akf", "skf", b, o, "path", 4}; - std::string generatedHostUrl = pathReq.getHostUrl(); - ASSERT_EQ(generatedHostUrl, "https://s3-service.com:443/test-bucket/test-object"); + // Test path-style URL generation + TestAmazonRequest pathReq{serviceUrl, "akf", "skf", b, o, "path", 4}; + std::string generatedHostUrl = pathReq.getHostUrl(); + ASSERT_EQ(generatedHostUrl, + "https://s3-service.com:443/test-bucket/test-object"); - // Test virtual-style URL generation - TestAmazonRequest virtReq{serviceUrl, "akf", "skf", b, o, "virtual", 4}; - generatedHostUrl = virtReq.getHostUrl(); - ASSERT_EQ(generatedHostUrl, "https://test-bucket.s3-service.com:443/test-object"); + // Test virtual-style URL generation + TestAmazonRequest virtReq{serviceUrl, "akf", "skf", b, o, "virtual", 4}; + generatedHostUrl = virtReq.getHostUrl(); + ASSERT_EQ(generatedHostUrl, + "https://test-bucket.s3-service.com:443/test-object"); - // Test path-style with empty bucket (which we use for exporting an entire endpoint) - TestAmazonRequest pathReqNoBucket{serviceUrl, "akf", "skf", "", o, "path", 4}; - generatedHostUrl = pathReqNoBucket.getHostUrl(); - ASSERT_EQ(generatedHostUrl, "https://s3-service.com:443/test-object"); + // Test path-style with empty bucket (which we use for exporting an entire + // endpoint) + TestAmazonRequest pathReqNoBucket{serviceUrl, "akf", "skf", "", + o, "path", 4}; + generatedHostUrl = pathReqNoBucket.getHostUrl(); + ASSERT_EQ(generatedHostUrl, "https://s3-service.com:443/test-object"); } int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); } From 739dce55b3c42b15cc3afd5687e9bc7893ad1a1d Mon Sep 17 00:00:00 2001 From: Justin Hiemstra Date: Tue, 16 Apr 2024 19:11:37 +0000 Subject: [PATCH 5/5] Unlink XrdHTTPServer from s3-gtest --- test/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8f5589b..438758c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -32,7 +32,7 @@ else() set(LIBGTEST "${CMAKE_BINARY_DIR}/external/gtest/src/gtest-build/lib/libgtest.a") endif() -target_link_libraries(s3-gtest XrdHTTPServer XrdS3 "${LIBGTEST}" pthread) +target_link_libraries(s3-gtest XrdS3 "${LIBGTEST}" pthread) target_link_libraries(http-gtest XrdHTTPServer "${LIBGTEST}" pthread)