From c20581dbcf20e690e4063f1de5454acac444e181 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Fri, 22 May 2026 09:19:52 +0200 Subject: [PATCH 1/3] [http] move actual tests to test_suite.cxx Let reuse same tests with different engine --- net/http/test/test_server.cxx | 125 ++-------------------------------- net/http/test/test_suite.cxx | 125 ++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 120 deletions(-) create mode 100644 net/http/test/test_suite.cxx diff --git a/net/http/test/test_server.cxx b/net/http/test/test_server.cxx index a4a5404462124..fd38017710b1f 100644 --- a/net/http/test/test_server.cxx +++ b/net/http/test/test_server.cxx @@ -12,42 +12,7 @@ #include "ROOT/TestSupport.hxx" -Int_t httpport = 0; -TString server_url; - - -// main http server -std::string execute_request(const char *url, const char *post = nullptr) -{ - TString fname = TString::Format("http_server_%d.output", httpport), - pname, exec; - - if (post) { - pname = TString::Format("http_server_%d.post", httpport); - std::ofstream f(pname.Data()); - f << post; - } - - if (post) - exec = TString::Format("curl -sS -X POST '%s%s' --data-binary @%s -o %s", server_url.Data(), url, pname.Data(), fname.Data()); - else - exec = TString::Format("curl -sS '%s%s' -o %s", server_url.Data(), url, fname.Data()); - - printf("Execute %s\n", exec.Data()); - - std::string res; - - if (gSystem->Exec(exec) != 0) - res = ""; - else - res = THttpServer::ReadFileContent(fname.Data()); - - gSystem->Unlink(fname); - if (!pname.IsNull()) - gSystem->Unlink(pname); - - return res; -} +#include "./test_suite.cxx" // main http server TEST(THttpServer, main) @@ -56,6 +21,8 @@ TEST(THttpServer, main) gRandom->SetSeed(0); + Int_t httpport = 0; + for(int ntry = 0; ntry < 100; ++ntry) { Int_t port = (Int_t) (25000 + gRandom->Rndm() * 1000); // only two threads, bind to loopback address only @@ -71,90 +38,8 @@ TEST(THttpServer, main) if (!httpport) return; + server_hash = httpport; server_url = TString::Format("http:/localhost:%d", httpport); - TNamed obj1("obj1", "title1"); - TNamed obj2("obj2", "title2"); - - serv.Register("/", &obj1); - serv.Register("/", &obj2); - - // let process requests in separate thread - serv.CreateServerThread(); - - // check JSON representation for the object - std::string res = execute_request("/obj1/root.json"); - EXPECT_EQ(res, "{\n" - " \"_typename\" : \"TNamed\",\n" - " \"fUniqueID\" : 0,\n" - " \"fBits\" : 8,\n" - " \"fName\" : \"obj1\",\n" - " \"fTitle\" : \"title1\"\n" - "}") << "result of root.json request"; - - // check XML representation for the object - res = execute_request("/obj1/root.xml"); - EXPECT_EQ(res, "\n" - " \n" - " \n" - " \n" - " \n" - " \n" - "\n") << "result of root.xml request"; - - - // check BINARY representation for the object - res = execute_request("/obj1/root.bin"); - // keep minimal margin for binary format change - EXPECT_NEAR(res.length(), 28, 4) << "size of root.bin request"; - - // check ROOT file creation - res = execute_request("/obj1/file.root"); - // TODO: anlyze why so big size for small object - EXPECT_NEAR(res.length(), 1024, 100) << "size of file.root request"; - - // check item request hierarchy request - res = execute_request("/obj1/item.json"); - - EXPECT_EQ(res, "{\n" - " \"_name\" : \"obj1\",\n" - " \"_root_version\" : " + std::to_string(gROOT->GetVersionCode()) + ",\n" - " \"_kind\" : \"ROOT.TNamed\",\n" - " \"_title\" : \"title1\"\n" - "}") << "result of item.json request"; - - - // check multi request to several objects - res = execute_request("/multi.json?number=2", "/obj1/root.json\n/obj2/root.json\n"); - EXPECT_EQ(res, "[{\n" - " \"_typename\" : \"TNamed\",\n" - " \"fUniqueID\" : 0,\n" - " \"fBits\" : 8,\n" - " \"fName\" : \"obj1\",\n" - " \"fTitle\" : \"title1\"\n" - "}, {\n" - " \"_typename\" : \"TNamed\",\n" - " \"fUniqueID\" : 0,\n" - " \"fBits\" : 8,\n" - " \"fName\" : \"obj2\",\n" - " \"fTitle\" : \"title2\"\n" - "}]") << "result of multi.json request"; - - - // by default methods execution is not allowed - res = execute_request("/obj1/exe.json?method=GetTitle"); - EXPECT_EQ(res, "") << "exe.json should be empty in readonly"; - - - // disable readonly to get extra functionality - serv.SetReadOnly(kFALSE); - - // only now one can execute method - res = execute_request("/obj1/exe.json?method=GetTitle"); - EXPECT_EQ(res, "\"title1\"") << "result of exe.json with object title"; - - res = execute_request("/obj1/exe.json?method=SetTitle&title=NewTitle"); - EXPECT_EQ(res, "null") << "returns null when executes void method"; - EXPECT_EQ(std::string("NewTitle"), obj1.GetTitle()) << "title must match with submitted value"; - obj1.SetTitle("title1"); + test_suite(serv); } diff --git a/net/http/test/test_suite.cxx b/net/http/test/test_suite.cxx new file mode 100644 index 0000000000000..dc0c2c21ac9f1 --- /dev/null +++ b/net/http/test/test_suite.cxx @@ -0,0 +1,125 @@ +Int_t server_hash = 0; +TString server_url; +TString unix_socket; + +// submit requests to server +std::string execute_request(const char *url, const char *post = nullptr) +{ + TString fname = TString::Format("http_server_%d.output", server_hash), + pname, exec; + + if (post) { + pname = TString::Format("http_server_%d.post", server_hash); + std::ofstream f(pname.Data()); + f << post; + } + + if (post) + exec = TString::Format("curl -sS -X POST %s '%s%s' --data-binary @%s -o %s", unix_socket.Data(), server_url.Data(), url, pname.Data(), fname.Data()); + else + exec = TString::Format("curl -sS %s '%s%s' -o %s", unix_socket.Data(), server_url.Data(), url, fname.Data()); + + printf("Execute %s\n", exec.Data()); + + std::string res; + + if (gSystem->Exec(exec) != 0) + res = ""; + else + res = THttpServer::ReadFileContent(fname.Data()); + + gSystem->Unlink(fname); + if (!pname.IsNull()) + gSystem->Unlink(pname); + + return res; +} + + +void test_suite(THttpServer &serv) +{ + // let process requests in separate thread + serv.CreateServerThread(); + + TNamed obj1("obj1", "title1"); + TNamed obj2("obj2", "title2"); + + serv.Register("/", &obj1); + serv.Register("/", &obj2); + + // check JSON representation for the object + std::string res = execute_request("/obj1/root.json"); + EXPECT_EQ(res, "{\n" + " \"_typename\" : \"TNamed\",\n" + " \"fUniqueID\" : 0,\n" + " \"fBits\" : 8,\n" + " \"fName\" : \"obj1\",\n" + " \"fTitle\" : \"title1\"\n" + "}") << "result of root.json request"; + + // check XML representation for the object + res = execute_request("/obj1/root.xml"); + EXPECT_EQ(res, "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n") << "result of root.xml request"; + + + // check BINARY representation for the object + res = execute_request("/obj1/root.bin"); + // keep minimal margin for binary format change + EXPECT_NEAR(res.length(), 28, 4) << "size of root.bin request"; + + // check ROOT file creation + res = execute_request("/obj1/file.root"); + // TODO: anlyze why so big size for small object + EXPECT_NEAR(res.length(), 1024, 100) << "size of file.root request"; + + // check item request hierarchy request + res = execute_request("/obj1/item.json"); + + EXPECT_EQ(res, "{\n" + " \"_name\" : \"obj1\",\n" + " \"_root_version\" : " + std::to_string(gROOT->GetVersionCode()) + ",\n" + " \"_kind\" : \"ROOT.TNamed\",\n" + " \"_title\" : \"title1\"\n" + "}") << "result of item.json request"; + + + // check multi request to several objects + res = execute_request("/multi.json?number=2", "/obj1/root.json\n/obj2/root.json\n"); + EXPECT_EQ(res, "[{\n" + " \"_typename\" : \"TNamed\",\n" + " \"fUniqueID\" : 0,\n" + " \"fBits\" : 8,\n" + " \"fName\" : \"obj1\",\n" + " \"fTitle\" : \"title1\"\n" + "}, {\n" + " \"_typename\" : \"TNamed\",\n" + " \"fUniqueID\" : 0,\n" + " \"fBits\" : 8,\n" + " \"fName\" : \"obj2\",\n" + " \"fTitle\" : \"title2\"\n" + "}]") << "result of multi.json request"; + + + // by default methods execution is not allowed + res = execute_request("/obj1/exe.json?method=GetTitle"); + EXPECT_EQ(res, "") << "exe.json should be empty in readonly"; + + + // disable readonly to get extra functionality + serv.SetReadOnly(kFALSE); + + // only now one can execute method + res = execute_request("/obj1/exe.json?method=GetTitle"); + EXPECT_EQ(res, "\"title1\"") << "result of exe.json with object title"; + + res = execute_request("/obj1/exe.json?method=SetTitle&title=NewTitle"); + EXPECT_EQ(res, "null") << "returns null when executes void method"; + EXPECT_EQ(std::string("NewTitle"), obj1.GetTitle()) << "title must match with submitted value"; + obj1.SetTitle("title1"); +} From 3eabf0740b3d808e43303a788c0b948c32a44954 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Fri, 22 May 2026 09:21:26 +0200 Subject: [PATCH 2/3] [http] add unix socket test Open random unix socket and try communication with curl. Require minimum curl 7.50 --- net/http/test/CMakeLists.txt | 5 +++- net/http/test/test_xsocket.cxx | 54 ++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 net/http/test/test_xsocket.cxx diff --git a/net/http/test/CMakeLists.txt b/net/http/test/CMakeLists.txt index ea6467e650b2f..8723eb153f70e 100644 --- a/net/http/test/CMakeLists.txt +++ b/net/http/test/CMakeLists.txt @@ -34,7 +34,10 @@ execute_process( # curl introduce support from 8.11 but it works reliably only with 8.18 if(CURL_CLI_OUTPUT MATCHES "curl ([0-9]+\\.[0-9]+\\.[0-9]+)") set(CURL_CLI_VERSION "${CMAKE_MATCH_1}") - message(STATUS "Find curl ${CURL_CLI_VERSION} for http server tests") + message(STATUS "Found curl ${CURL_CLI_VERSION} for http server tests") + if(CURL_CLI_VERSION VERSION_GREATER_EQUAL "7.50.0") + ROOT_ADD_GTEST(testUnixSocket test_xsocket.cxx LIBRARIES RHTTP RHTTPSniff Hist) + endif() if(CURL_CLI_VERSION VERSION_GREATER_EQUAL "8.18.0") ROOT_ADD_GTEST(testWebSocket test_websocket.cxx LIBRARIES RHTTP Hist) endif() diff --git a/net/http/test/test_xsocket.cxx b/net/http/test/test_xsocket.cxx new file mode 100644 index 0000000000000..a3b1ea696a4b0 --- /dev/null +++ b/net/http/test/test_xsocket.cxx @@ -0,0 +1,54 @@ +#include "gtest/gtest.h" + +#include +#include + +#include "THttpServer.h" +#include "TROOT.h" + +#include "TSystem.h" +#include "TNamed.h" +#include "TRandom.h" + +#include "ROOT/TestSupport.hxx" + + +#include "./test_suite.cxx" + +// main http server +TEST(THttpServer, main) +{ + THttpServer serv(""); + + TString fname; + Bool_t create = kFALSE; + + for (Int_t ntry = 0; ntry < 10; ++ntry) { + fname = "root_test_"; + auto f = gSystem->TempFileName(fname, "/tmp", ".socket"); + if (!f) + continue; + fclose(f); + + EXPECT_EQ(fname[0], '/') << "first symbol in temporary file must be slash"; + create = serv.CreateEngine("socket:" + fname + "?socket_mode=0700&thrd=2"); + + if (create) + break; + + gSystem->Unlink(fname); + } + + EXPECT_EQ(create, kTRUE) << "create unix socket"; + + if (!create) + return; + + server_hash = fname.Hash(); // dummy number for file suffix + unix_socket = "--unix-socket " + fname; // curl argument + server_url = "http://localhost"; // dummy host name for curl + + test_suite(serv); + + gSystem->Unlink(fname); // cleanup socket file +} From f79bf967f125f244511c9c91089594ed20e52ef5 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Fri, 22 May 2026 09:32:04 +0200 Subject: [PATCH 3/3] [http] mention unix sockets in class docu --- net/http/src/THttpServer.cxx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/net/http/src/THttpServer.cxx b/net/http/src/THttpServer.cxx index ea9483733f310..d29a2523bd377 100644 --- a/net/http/src/THttpServer.cxx +++ b/net/http/src/THttpServer.cxx @@ -106,6 +106,7 @@ More information: https://root.cern/root/htmldoc/guides/HttpServer/HttpServer.ht /// at once, separating them with semicolon (";"). Following engines are supported: /// /// http - TCivetweb, civetweb-based implementation of http protocol +/// socket - TCivetweb bound to unix socket /// fastcgi - TFastCgi, special protocol for communicating with web servers /// /// For each created engine one should provide socket port number like "http:8080" or "fastcgi:9000". @@ -411,6 +412,8 @@ void THttpServer::SetDrawPage(const std::string &filename) /// serv->CreateEngine("http:8080"); /// serv->CreateEngine("civetweb:8080"); /// serv->CreateEngine(":8080"); +/// // creates civetweb web server bound with unix socket +/// serv->CreateEngine("socket:/home/user/server.socket?socket_mode=0700"); /// // creates fastcgi server with port 9000 /// serv->CreateEngine("fastcgi:9000"); ///