Skip to content

Commit 89dc944

Browse files
cassavaclsim
andcommitted
oak: Add several unit tests for server
These unfortunately require curl to be available. When this becomes a problem, we may need to disable them or find another work-around. Right now, they provide value. Co-authored-by: Martin Henselmeyer <[email protected]>
1 parent 3171837 commit 89dc944

File tree

4 files changed

+301
-0
lines changed

4 files changed

+301
-0
lines changed

oak/CMakeLists.txt

+6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ add_library(${target}
2222
# For IDE integration
2323
${${target}_PUBLIC_HEADERS}
2424
src/oak/request_stub.hpp
25+
src/oak/curl.hpp
2526
)
2627
add_library(${alias} ALIAS ${target})
2728
set_target_linting(${target})
@@ -52,11 +53,16 @@ if(BUILD_TESTING)
5253
add_executable(test-oak
5354
# find src -type f -name "*_test.cpp"
5455
src/oak/route_muxer_test.cpp
56+
src/oak/server_test.cpp
5557
)
5658
set_target_properties(test-oak PROPERTIES
5759
CXX_STANDARD 14
5860
CXX_STANDARD_REQUIRED ON
5961
)
62+
target_include_directories(test-oak
63+
PRIVATE
64+
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>"
65+
)
6066
target_link_libraries(test-oak
6167
GTest::gtest
6268
GTest::gtest_main

oak/include/oak/server.hpp

+7
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ class Server {
101101
/**
102102
* Set the number of threads used for listening to connections.
103103
* This is the number of requests that can be handled simultaneously.
104+
*
105+
* NOTE: This doesn't do anything at the moment; a new thread is created
106+
* for each connection.
104107
*/
105108
void set_threads(int n) { listen_threads_ = n; }
106109

@@ -114,11 +117,15 @@ class Server {
114117
*/
115118
void set_address(const std::string& addr) { listen_addr_ = addr; }
116119

120+
const std::string& address() const { return listen_addr_; }
121+
117122
/**
118123
* Set the port on which to listen.
119124
*/
120125
void set_port(int port) { listen_port_ = port; }
121126

127+
int port() const { return listen_port_; }
128+
122129
/**
123130
* Returns whether the server has started and is currently listening.
124131
*/

oak/src/oak/curl.hpp

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2023 Robert Bosch GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
/**
19+
* \file oak/curl.hpp
20+
*/
21+
22+
#pragma once
23+
24+
#include <string>
25+
#include <vector>
26+
27+
namespace oak {
28+
29+
struct Curl {
30+
std::string method;
31+
std::string address;
32+
int port;
33+
std::string endpoint;
34+
std::string data;
35+
std::vector<std::string> headers;
36+
37+
public:
38+
static Curl get(const std::string& address, int port, const std::string& endpoint) {
39+
Curl c;
40+
c.method = "GET";
41+
c.address = address;
42+
c.port = port;
43+
c.endpoint = endpoint;
44+
return std::move(c);
45+
}
46+
47+
static Curl post(const std::string& address, int port, const std::string& endpoint,
48+
const std::string& data, const std::string& mime_type) {
49+
Curl c;
50+
c.method = "POST";
51+
c.address = address;
52+
c.port = port;
53+
c.endpoint = endpoint;
54+
c.data = data;
55+
c.headers.push_back(std::string("Content-Type: ") + mime_type);
56+
return c;
57+
}
58+
59+
std::string to_string() const {
60+
std::string buf;
61+
buf += "curl -q";
62+
buf += " -X " + method;
63+
for (const auto& h : headers) {
64+
buf += " -H '" + h + "'";
65+
}
66+
if (data != "") {
67+
buf += " -d '" + data + "'";
68+
}
69+
buf += " http://" + address + ":" + std::to_string(port);
70+
if (endpoint != "" && endpoint[0] != '/') {
71+
buf += "/";
72+
}
73+
buf += endpoint;
74+
return buf;
75+
}
76+
};
77+
78+
} // namespace oak

oak/src/oak/server_test.cpp

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*
2+
* Copyright 2022 Robert Bosch GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
/**
19+
* \file oak/server_test.cpp
20+
* \see oak/server_test.hpp
21+
*/
22+
23+
#include <gtest/gtest.h> // for TEST, EXPECT_TRUE, ...
24+
25+
#include <iostream>
26+
#include <map> // for map<>
27+
#include <string> // for string
28+
#include <utility> // for tie
29+
#include <vector> // for vector<>
30+
31+
#include <array>
32+
#include <cstdio>
33+
#include <iostream>
34+
#include <memory>
35+
#include <stdexcept>
36+
#include <string>
37+
38+
#include <cloe/registrar.hpp>
39+
#include <fable/utility/gtest.hpp>
40+
41+
#include "oak/registrar.hpp" // for Registrar
42+
#include "oak/server.hpp" // for Server
43+
#include "oak/curl.hpp" // for Curl
44+
45+
using namespace std; // NOLINT(build/namespaces)
46+
47+
/**
48+
* Executes a (shell) command and return the StdOut
49+
*/
50+
std::string exec(const char* cmd) {
51+
std::cerr << "Exec: " << cmd << std::endl;
52+
53+
std::array<char, 128> buffer;
54+
std::string result;
55+
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd, "r"), pclose);
56+
if (!pipe) {
57+
throw std::runtime_error("popen() failed!");
58+
}
59+
while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
60+
result += buffer.data();
61+
}
62+
return result;
63+
}
64+
65+
std::string exec(const std::string& cmd) { return exec(cmd.c_str()); }
66+
std::string exec(const oak::Curl& curl) { return exec(curl.to_string()); }
67+
68+
std::string address;
69+
std::atomic<unsigned short> port;
70+
71+
class Environment : public ::testing::Environment {
72+
public:
73+
~Environment() override {}
74+
void SetUp() override {
75+
address = "127.0.0.1";
76+
port = 12340;
77+
}
78+
void TearDown() override {}
79+
};
80+
testing::Environment* const foo_env = testing::AddGlobalTestEnvironment(new Environment);
81+
82+
std::unique_ptr<oak::Server> create_server() {
83+
int retry = 0;
84+
do {
85+
try {
86+
auto port_ = port++;
87+
std::cout << "Trying " << address << ":" << port_ << std::endl;
88+
auto server = std::make_unique<oak::Server>(address, port_);
89+
server->listen();
90+
// if server is listening, return otherwise retry
91+
if (server->is_listening()) {
92+
return server;
93+
} else {
94+
retry++;
95+
}
96+
} catch (...) {
97+
EXPECT_TRUE(false) << "Unexpected exception";
98+
}
99+
} while (retry < 100);
100+
101+
return nullptr;
102+
}
103+
104+
/**
105+
* Try to create a server.
106+
*/
107+
TEST(oak_server, listen) { auto server = create_server(); }
108+
109+
/**
110+
* Test that stopping a not-listening server results in an exception.
111+
*/
112+
TEST(oak_server, fail_not_listening) {
113+
oak::Server server;
114+
EXPECT_THROW(server.stop(), std::runtime_error);
115+
}
116+
117+
/**
118+
* Try to GET a non-existing endpoint.
119+
*/
120+
TEST(oak_server, get_nohandler) {
121+
auto server = create_server();
122+
ASSERT_NE(server, nullptr);
123+
auto address_ = server->address();
124+
auto port_ = server->port();
125+
126+
auto result = exec(oak::Curl::get(address_, port_, "test"));
127+
128+
// Compare result
129+
fable::assert_eq(fable::parse_json(result.c_str()), R"({
130+
"endpoints": [],
131+
"error": "cannot find handler"
132+
})");
133+
}
134+
135+
/**
136+
* Try to GET an endpoint.
137+
*/
138+
TEST(oak_server, get_handler) {
139+
auto server = create_server();
140+
ASSERT_NE(server, nullptr);
141+
auto address_ = server->address();
142+
auto port_ = server->port();
143+
144+
// Create registrar
145+
oak::StaticRegistrar registrar(server.get(), "", nullptr);
146+
// Register one pseudo-endpoint
147+
std::vector<std::string> data{ "1", "2", "3" };
148+
registrar.register_handler("/simulators", cloe::handler::StaticJson(data));
149+
150+
// Read the pseudo-endpoint
151+
auto result = exec(oak::Curl::get(address_, port_, "simulators"));
152+
153+
// Compare result
154+
fable::assert_eq(fable::parse_json(result.c_str()), R"([
155+
"1",
156+
"2",
157+
"3"
158+
])");
159+
}
160+
161+
/**
162+
* @brief Tries to POST to an endpoint which echos the data back
163+
*/
164+
TEST(oak_server, post_handler) {
165+
auto server = create_server();
166+
ASSERT_NE(server, nullptr);
167+
auto address_ = server->address();
168+
auto port_ = server->port();
169+
170+
// Create registrar
171+
oak::StaticRegistrar registrar(server.get(), "", nullptr);
172+
// Register one pseudo-endpoint
173+
std::vector<std::string> data{
174+
"1",
175+
"2",
176+
"3",
177+
};
178+
registrar.register_handler(
179+
"/echo", [this](const cloe::Request& request, cloe::Response& response) {
180+
switch (request.method()) {
181+
case cloe::RequestMethod::POST: {
182+
try {
183+
const auto json = request.as_json();
184+
response.write(json);
185+
response.set_status(cloe::StatusCode::OK);
186+
} catch (const std::exception& ex) {
187+
response.bad_request(cloe::Json{
188+
{"error", ex.what()},
189+
});
190+
}
191+
break;
192+
}
193+
default: {
194+
response.not_allowed(cloe::RequestMethod::POST,
195+
cloe::Json{
196+
{"error", "only GET or POST method allowed"},
197+
});
198+
}
199+
}
200+
});
201+
202+
// Read the pseudo-endpoint
203+
auto result = exec(oak::Curl::post(address_, port_, "echo", R"({"a": "b", "c": "d"})", "application/json"));
204+
205+
// Compare result
206+
fable::assert_eq(fable::parse_json(result.c_str()), R"({
207+
"a": "b",
208+
"c": "d"
209+
})");
210+
}

0 commit comments

Comments
 (0)