Skip to content

Commit 6ff0273

Browse files
committed
test(sdks): raise coverage across Python/JS/C#/Java/C++ SDKs
Coverage push to clear the 80% bar per SDK (local, statement/line): - python 74% -> 94%: fake-agent harness over real sockets driving transport/tcp.py 48%->99% and client branches 69%->98% (TLS matrix included) - js ~78% -> 97%: tcp_transport 12.7%->96.8% via an in-memory fake agent (framing, multiplexing, inbound dispatch, disconnects); invoker error/streaming matrix 78.6%->98.6%; index.ts -> 100% - csharp 61.4% -> 94.3%: MockAgentServer + lifecycle/transport-factory tests lift CroupierClient to 94% and Invoker to 100% - java 84% -> 97.4% (jacoco line): client 62%->96%, ServerHttpInvoker 70%->95%, transport edge scripts; jacoco config added - cpp 64.4% -> 88.1% (gcov): http_transport 0%->88%, client 55%->82%, dynamic_loader 48%->94% via real-socket/real-.so tests; 471 gtests Bugs found are recorded in the task notes and left unfixed (TLS race in python transport, C# heartbeat-loop kill on manual disconnect, java JSON integer widening, C++ single-shot reconnect, among others).
1 parent f168dbd commit 6ff0273

31 files changed

Lines changed: 9033 additions & 50048 deletions

sdks/cpp/CMakeLists.txt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,8 +684,30 @@ if(BUILD_TESTS)
684684
tests/test_plugin_manager.cpp
685685
tests/test_protocol.cpp
686686
tests/test_logger.cpp
687+
tests/test_http_transport.cpp
688+
tests/test_client_provider.cpp
689+
tests/test_dynamic_loader_lifecycle.cpp
690+
tests/test_json_schema.cpp
691+
tests/test_config_generation.cpp
692+
tests/test_invoker_retry.cpp
687693
)
688694

695+
# Real shared-object plugins used by the dynamic loader tests.
696+
if(UNIX)
697+
add_library(croupier-sample-plugin SHARED tests/plugins/sample_plugin.cpp)
698+
target_include_directories(croupier-sample-plugin PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
699+
set_target_properties(croupier-sample-plugin PROPERTIES
700+
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/test-plugins
701+
PREFIX "lib"
702+
)
703+
add_library(croupier-failing-init-plugin SHARED tests/plugins/failing_init_plugin.cpp)
704+
target_include_directories(croupier-failing-init-plugin PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
705+
set_target_properties(croupier-failing-init-plugin PROPERTIES
706+
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/test-plugins
707+
PREFIX "lib"
708+
)
709+
endif()
710+
689711
add_executable(croupier-sdk-tests
690712
${CROUPIER_TEST_SOURCES}
691713
)
@@ -695,6 +717,12 @@ if(BUILD_TESTS)
695717
target_compile_definitions(croupier-sdk-tests PRIVATE CROUPIER_SDK_HAS_TCP)
696718
endif()
697719

720+
if(UNIX)
721+
add_dependencies(croupier-sdk-tests croupier-sample-plugin croupier-failing-init-plugin)
722+
target_compile_definitions(croupier-sdk-tests PRIVATE
723+
CROUPIER_TEST_PLUGIN_DIR="${CMAKE_BINARY_DIR}/test-plugins")
724+
endif()
725+
698726
if(TARGET GTest::gtest_main)
699727
set(_croupier_gtest_main GTest::gtest_main)
700728
else()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Test-only plugin whose croupier_plugin_init reports failure. Used to cover
2+
// PluginManager::InitializePlugin failure branches.
3+
#include "croupier/sdk/plugin/dynamic_loader.h"
4+
5+
using namespace croupier::sdk::plugin;
6+
7+
static PluginInfo failing_info = {
8+
"failing_init_plugin",
9+
"1.0.0",
10+
"SDK Tests",
11+
"Plugin whose init always fails",
12+
{},
13+
{},
14+
};
15+
16+
extern "C" {
17+
18+
int croupier_plugin_init() { return 7; }
19+
20+
PluginInfo* croupier_plugin_info() { return &failing_info; }
21+
22+
void croupier_plugin_cleanup() {}
23+
24+
} // extern "C"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Test-only sample plugin used by unit tests to exercise the dynamic loader
2+
// and plugin manager against a real shared object.
3+
#include "croupier/sdk/plugin/dynamic_loader.h"
4+
5+
#include <string>
6+
7+
using namespace croupier::sdk::plugin;
8+
9+
static PluginInfo sample_info = {
10+
"sample_plugin",
11+
"2.1.0",
12+
"SDK Tests",
13+
"Plugin used by croupier-sdk-tests",
14+
{"sample_echo", "sample_missing"},
15+
{{"language", "C++"}},
16+
};
17+
18+
static int g_init_calls = 0;
19+
20+
extern "C" {
21+
22+
int sample_plugin_init_calls() { return g_init_calls; }
23+
24+
int croupier_plugin_init() {
25+
++g_init_calls;
26+
return 0;
27+
}
28+
29+
PluginInfo* croupier_plugin_info() { return &sample_info; }
30+
31+
void croupier_plugin_cleanup() {}
32+
33+
const char* sample_echo(const char* context, const char* payload) {
34+
static std::string result;
35+
result = std::string("{\"echo\":true,\"context\":\"") + (context ? context : "") +
36+
"\",\"payload\":" + (payload ? payload : "null") + "}";
37+
return result.c_str();
38+
}
39+
40+
} // extern "C"
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
// Provider-side client lifecycle against a fake Agent built from the SDK's own
2+
// TCPServer: registration, heartbeats, Serve() loop, and reconnection.
3+
#include <gtest/gtest.h>
4+
#include "croupier/sdk/croupier_client.h"
5+
#include "croupier/sdk/tcp_transport.h"
6+
#include "croupier/sdk/v1/provider.pb.h"
7+
8+
#include <atomic>
9+
#include <chrono>
10+
#include <memory>
11+
#include <thread>
12+
13+
namespace croupier::sdk::test {
14+
namespace {
15+
16+
using ::croupier::sdk::v1::ProviderConnectRequest;
17+
using ::croupier::sdk::v1::ProviderConnectResponse;
18+
using ::croupier::sdk::v1::ProviderHeartbeatRequest;
19+
20+
enum class AgentMode {
21+
kAccept, // normal registration with a session id
22+
kJsonError, // respond with a JSON error document
23+
kEmptySessionId, // respond with a protobuf response lacking session_id
24+
};
25+
26+
// Picks a free loopback port so a restarted fake agent rebinds the same port.
27+
int ReserveLoopbackPort() {
28+
TCPServer probe("127.0.0.1:0");
29+
probe.Start();
30+
const std::string address = probe.GetListenAddress();
31+
probe.Stop();
32+
return std::stoi(address.substr(address.rfind(':') + 1));
33+
}
34+
35+
class FakeAgent {
36+
public:
37+
explicit FakeAgent(AgentMode mode = AgentMode::kAccept)
38+
: mode_(mode), server_(/*listen_address=*/"127.0.0.1:" + std::to_string(ReserveLoopbackPort()),
39+
/*timeout_ms=*/5000) {
40+
server_.SetHandler([this](uint32_t msg_type, uint32_t /*req_id*/, const std::vector<uint8_t>& body) {
41+
if (msg_type == protocol::MSG_PROVIDER_CONNECT_REQUEST) {
42+
++connect_requests_;
43+
ProviderConnectRequest request;
44+
(void)request.ParseFromArray(body.data(), static_cast<int>(body.size()));
45+
last_service_id_ = request.service_id();
46+
last_function_count_ = static_cast<int>(request.functions_size());
47+
48+
if (mode_ == AgentMode::kJsonError) {
49+
const std::string json_error = R"({"error":"rejected"})";
50+
return std::vector<uint8_t>(json_error.begin(), json_error.end());
51+
}
52+
ProviderConnectResponse response;
53+
if (mode_ == AgentMode::kAccept) {
54+
response.set_session_id("session-42");
55+
}
56+
std::string out;
57+
response.SerializeToString(&out);
58+
return std::vector<uint8_t>(out.begin(), out.end());
59+
}
60+
if (msg_type == protocol::MSG_PROVIDER_HEARTBEAT_REQUEST) {
61+
++heartbeats_;
62+
ProviderHeartbeatRequest request;
63+
(void)request.ParseFromArray(body.data(), static_cast<int>(body.size()));
64+
last_heartbeat_session_ = request.session_id();
65+
return std::vector<uint8_t>{};
66+
}
67+
return std::vector<uint8_t>{};
68+
});
69+
server_.Start();
70+
}
71+
72+
~FakeAgent() { server_.Stop(); }
73+
74+
std::string address() const { return server_.GetListenAddress(); }
75+
int port() const {
76+
const size_t colon = address().rfind(':');
77+
return std::stoi(address().substr(colon + 1));
78+
}
79+
int heartbeats() const { return heartbeats_; }
80+
int connect_requests() const { return connect_requests_; }
81+
const std::string& last_service_id() const { return last_service_id_; }
82+
const std::string& last_heartbeat_session() const { return last_heartbeat_session_; }
83+
int last_function_count() const { return last_function_count_; }
84+
85+
void stop() { server_.Stop(); }
86+
void restart() { server_.Start(); }
87+
88+
private:
89+
AgentMode mode_;
90+
TCPServer server_;
91+
std::atomic<int> heartbeats_{0};
92+
std::atomic<int> connect_requests_{0};
93+
std::string last_service_id_;
94+
std::string last_heartbeat_session_;
95+
std::atomic<int> last_function_count_{0};
96+
};
97+
98+
ClientConfig ProviderConfig(const std::string& agent_addr, int heartbeat_interval = 1) {
99+
ClientConfig config;
100+
config.game_id = "game-test";
101+
config.env = "development";
102+
config.service_id = "cpp-tests";
103+
config.agent_addr = agent_addr;
104+
config.timeout_seconds = 5;
105+
config.connect_timeout_seconds = 2;
106+
config.heartbeat_interval = heartbeat_interval;
107+
config.disable_logging = true;
108+
return config;
109+
}
110+
111+
void RegisterSampleFunction(CroupierClient& client) {
112+
FunctionDescriptor desc;
113+
desc.id = "test.echo";
114+
desc.version = "1.4.2";
115+
desc.summary = "echo payload";
116+
desc.operation = "echo";
117+
desc.capability = "action";
118+
desc.risk = "safe";
119+
FunctionHandler handler = [](const std::string&, const std::string& payload) { return payload; };
120+
ASSERT_TRUE(client.RegisterFunction(desc, handler));
121+
}
122+
123+
TEST(ProviderLifecycleTest, ConnectRegistersFunctionsAndReceivesSession) {
124+
FakeAgent agent;
125+
CroupierClient client(ProviderConfig(agent.address(), /*heartbeat_interval=*/60));
126+
RegisterSampleFunction(client);
127+
128+
EXPECT_FALSE(client.IsConnected());
129+
ASSERT_TRUE(client.Connect());
130+
EXPECT_TRUE(client.IsConnected());
131+
ASSERT_EQ(1, agent.connect_requests());
132+
EXPECT_EQ("cpp-tests", agent.last_service_id());
133+
ASSERT_EQ(1, agent.last_function_count());
134+
client.Stop();
135+
EXPECT_FALSE(client.IsConnected());
136+
std::this_thread::sleep_for(std::chrono::milliseconds(150));
137+
}
138+
139+
TEST(ProviderLifecycleTest, ConnectWithoutRegisteredFunctionsFails) {
140+
FakeAgent agent;
141+
CroupierClient client(ProviderConfig(agent.address()));
142+
EXPECT_FALSE(client.Connect());
143+
EXPECT_EQ(0, agent.connect_requests());
144+
}
145+
146+
TEST(ProviderLifecycleTest, AgentJsonErrorFailsRegistration) {
147+
FakeAgent agent(AgentMode::kJsonError);
148+
CroupierClient client(ProviderConfig(agent.address()));
149+
RegisterSampleFunction(client);
150+
EXPECT_FALSE(client.Connect());
151+
EXPECT_FALSE(client.IsConnected());
152+
}
153+
154+
TEST(ProviderLifecycleTest, EmptySessionIdFailsRegistration) {
155+
FakeAgent agent(AgentMode::kEmptySessionId);
156+
CroupierClient client(ProviderConfig(agent.address()));
157+
RegisterSampleFunction(client);
158+
EXPECT_FALSE(client.Connect());
159+
EXPECT_FALSE(client.IsConnected());
160+
}
161+
162+
TEST(ProviderLifecycleTest, DoubleConnectIsIdempotent) {
163+
FakeAgent agent;
164+
CroupierClient client(ProviderConfig(agent.address(), /*heartbeat_interval=*/60));
165+
RegisterSampleFunction(client);
166+
ASSERT_TRUE(client.Connect());
167+
ASSERT_TRUE(client.Connect());
168+
EXPECT_EQ(1, agent.connect_requests());
169+
client.Stop();
170+
std::this_thread::sleep_for(std::chrono::milliseconds(150));
171+
}
172+
173+
TEST(ProviderLifecycleTest, HeartbeatSendsSessionId) {
174+
FakeAgent agent;
175+
CroupierClient client(ProviderConfig(agent.address(), /*heartbeat_interval=*/1));
176+
RegisterSampleFunction(client);
177+
ASSERT_TRUE(client.Connect());
178+
179+
// Wait until at least one heartbeat reached the fake agent (bounded).
180+
for (int i = 0; i < 100 && agent.heartbeats() < 1; ++i) {
181+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
182+
}
183+
EXPECT_GE(agent.heartbeats(), 1);
184+
EXPECT_EQ("session-42", agent.last_heartbeat_session());
185+
186+
client.Stop();
187+
std::this_thread::sleep_for(std::chrono::milliseconds(200));
188+
}
189+
190+
TEST(ProviderLifecycleTest, ServeRunsUntilStop) {
191+
FakeAgent agent;
192+
CroupierClient client(ProviderConfig(agent.address(), /*heartbeat_interval=*/60));
193+
RegisterSampleFunction(client);
194+
195+
std::thread serve_thread([&client] { client.Serve(); });
196+
for (int i = 0; i < 100 && !client.IsConnected(); ++i) {
197+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
198+
}
199+
ASSERT_TRUE(client.IsConnected());
200+
201+
client.Stop();
202+
serve_thread.join();
203+
EXPECT_FALSE(client.IsConnected());
204+
std::this_thread::sleep_for(std::chrono::milliseconds(150));
205+
}
206+
207+
TEST(ProviderLifecycleTest, ReconnectsAfterAgentRestart) {
208+
FakeAgent agent;
209+
ClientConfig config = ProviderConfig(agent.address(), /*heartbeat_interval=*/1);
210+
config.timeout_seconds = 1; // fail fast so heartbeat errors surface quickly
211+
CroupierClient client(config);
212+
RegisterSampleFunction(client);
213+
ASSERT_TRUE(client.Connect());
214+
EXPECT_EQ(1, agent.connect_requests());
215+
216+
// Kill and immediately restart the agent on the same port. The client's
217+
// existing TCP session dies, so the next heartbeat fails and triggers a
218+
// reconnect, which can succeed because the listener is already back.
219+
// NOTE (recorded bug): after one failed reconnect attempt the client
220+
// stops retrying (Connect() failure sets should_stop_heartbeat_, which
221+
// terminates reconnectLoop), so a slower agent restart would never
222+
// recover.
223+
agent.stop();
224+
agent.restart();
225+
226+
bool reconnected = false;
227+
for (int i = 0; i < 300 && !reconnected; ++i) {
228+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
229+
reconnected = client.IsConnected() && agent.connect_requests() >= 2;
230+
}
231+
EXPECT_TRUE(reconnected);
232+
EXPECT_GE(agent.connect_requests(), 2);
233+
234+
client.Stop();
235+
std::this_thread::sleep_for(std::chrono::milliseconds(200));
236+
}
237+
238+
TEST(ProviderLifecycleTest, CloseClearsState) {
239+
FakeAgent agent;
240+
CroupierClient client(ProviderConfig(agent.address(), /*heartbeat_interval=*/60));
241+
RegisterSampleFunction(client);
242+
ASSERT_TRUE(client.Connect());
243+
client.Close();
244+
EXPECT_FALSE(client.IsConnected());
245+
std::this_thread::sleep_for(std::chrono::milliseconds(150));
246+
}
247+
248+
} // namespace
249+
} // namespace croupier::sdk::test

0 commit comments

Comments
 (0)