|
7 | 7 | #include <gtest/gtest.h> |
8 | 8 | #include "croupier/sdk/croupier_client.h" |
9 | 9 | #include "croupier/sdk/protocol.h" |
| 10 | +#include "croupier/agent/v1/register.pb.h" |
10 | 11 | #include "croupier/sdk/v1/provider.pb.h" |
11 | 12 | #include "croupier/sdk/v1/invocation.pb.h" |
12 | 13 |
|
|
17 | 18 | #include <string> |
18 | 19 | #include <thread> |
19 | 20 | #include <vector> |
| 21 | +#include <zlib.h> |
20 | 22 |
|
21 | 23 | #ifdef _WIN32 |
22 | 24 | #include <winsock2.h> |
@@ -428,6 +430,143 @@ TEST(ProviderInboundTest, AgentDrainIsIdempotent) { |
428 | 430 | } |
429 | 431 |
|
430 | 432 | } // namespace |
| 433 | + |
| 434 | +// ===== F:控制面 manifest 上传——端到端帧回路 ===== |
| 435 | + |
| 436 | +namespace { |
| 437 | + |
| 438 | +socket_t listen_tcp(unsigned short* out_port) { |
| 439 | + socket_t fd = ::socket(AF_INET, SOCK_STREAM, 0); |
| 440 | + if (fd == INVALID_SOCK) return INVALID_SOCK; |
| 441 | + sockaddr_in addr{}; |
| 442 | + addr.sin_family = AF_INET; |
| 443 | + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
| 444 | + addr.sin_port = 0; |
| 445 | + if (::bind(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0 || |
| 446 | + ::listen(fd, 1) != 0) { |
| 447 | + closesocket(fd); |
| 448 | + return INVALID_SOCK; |
| 449 | + } |
| 450 | + sockaddr_in bound{}; |
| 451 | + socklen_t blen = sizeof(bound); |
| 452 | + ::getsockname(fd, reinterpret_cast<sockaddr*>(&bound), &blen); |
| 453 | + *out_port = ntohs(bound.sin_port); |
| 454 | + return fd; |
| 455 | +} |
| 456 | + |
| 457 | +std::vector<uint8_t> recv_exact(socket_t conn, size_t len) { |
| 458 | + std::vector<uint8_t> out(len); |
| 459 | + size_t got = 0; |
| 460 | + while (got < len) { |
| 461 | + int n = static_cast<int>(::recv(conn, reinterpret_cast<char*>(out.data()) + got, |
| 462 | + static_cast<int>(len - got), 0)); |
| 463 | + if (n <= 0) return {}; |
| 464 | + got += static_cast<size_t>(n); |
| 465 | + } |
| 466 | + return out; |
| 467 | +} |
| 468 | + |
| 469 | +std::string gzip_decompress(const std::string& compressed) { |
| 470 | + std::string out; |
| 471 | + z_stream stream{}; |
| 472 | + if (inflateInit2(&stream, 15 + 16) != Z_OK) return out; |
| 473 | + stream.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(compressed.data())); |
| 474 | + stream.avail_in = static_cast<uInt>(compressed.size()); |
| 475 | + char buffer[4096]; |
| 476 | + int status = Z_OK; |
| 477 | + do { |
| 478 | + stream.next_out = reinterpret_cast<Bytef*>(buffer); |
| 479 | + stream.avail_out = sizeof(buffer); |
| 480 | + status = inflate(&stream, Z_NO_FLUSH); |
| 481 | + if (status != Z_OK && status != Z_STREAM_END && status != Z_BUF_ERROR) break; |
| 482 | + out.append(buffer, sizeof(buffer) - stream.avail_out); |
| 483 | + } while (status != Z_STREAM_END); |
| 484 | + inflateEnd(&stream); |
| 485 | + return out; |
| 486 | +} |
| 487 | + |
| 488 | +} // namespace |
| 489 | + |
| 490 | +TEST(ManifestUploadTest, UploadsGzippedManifestToControlPlane) { |
| 491 | + // Agent(复用 RawFakeAgent 完整握手)+ 控制面(独立监听) |
| 492 | + RawFakeAgent agent; |
| 493 | + std::thread agent_thread([&] { agent.AcceptAndHandshake(); }); |
| 494 | + |
| 495 | + unsigned short control_port = 0; |
| 496 | + socket_t control_fd = listen_tcp(&control_port); |
| 497 | + ASSERT_NE(control_fd, INVALID_SOCK); |
| 498 | + |
| 499 | + std::atomic<bool> got_manifest{false}; |
| 500 | + std::thread control_thread([&] { |
| 501 | + socket_t conn = ::accept(control_fd, nullptr, nullptr); |
| 502 | + if (conn == INVALID_SOCK) return; |
| 503 | + auto header = recv_exact(conn, 4); |
| 504 | + if (header.empty()) return; |
| 505 | + uint32_t len = (uint32_t(header[0]) << 24) | (uint32_t(header[1]) << 16) | |
| 506 | + (uint32_t(header[2]) << 8) | uint32_t(header[3]); |
| 507 | + auto frame_body = recv_exact(conn, len); |
| 508 | + constexpr size_t kHeaderSize = 8; // version(1) + msg_id(3) + req_id(4) |
| 509 | + if (frame_body.size() < kHeaderSize) return; |
| 510 | + uint32_t msg_id = protocol::GetMsgID(frame_body.data() + 1); |
| 511 | + ASSERT_EQ(msg_id, static_cast<unsigned>(protocol::MSG_REGISTER_CAPABILITIES_REQ)); |
| 512 | + uint32_t req_id = (uint32_t(frame_body[4]) << 24) | (uint32_t(frame_body[5]) << 16) | |
| 513 | + (uint32_t(frame_body[6]) << 8) | uint32_t(frame_body[7]); |
| 514 | + |
| 515 | + ::croupier::agent::v1::RegisterCapabilitiesRequest req; |
| 516 | + ASSERT_TRUE(req.ParseFromArray(frame_body.data() + kHeaderSize, |
| 517 | + static_cast<int>(frame_body.size() - kHeaderSize))); |
| 518 | + std::string decompressed = |
| 519 | + gzip_decompress(std::string(req.manifest_json_gz().begin(), |
| 520 | + req.manifest_json_gz().end())); |
| 521 | + EXPECT_NE(decompressed.find("\"provider\""), std::string::npos); |
| 522 | + EXPECT_NE(decompressed.find("player.ban"), std::string::npos); |
| 523 | + got_manifest.store(true); |
| 524 | + |
| 525 | + // 回确认帧 |
| 526 | + ::croupier::agent::v1::RegisterCapabilitiesResponse ack; |
| 527 | + std::string ack_out; |
| 528 | + ack.SerializeToString(&ack_out); |
| 529 | + auto resp_frame = protocol::NewMessage( |
| 530 | + protocol::GetResponseMsgID(msg_id), req_id, |
| 531 | + std::vector<uint8_t>(ack_out.begin(), ack_out.end())); |
| 532 | + std::vector<uint8_t> wrapped(4 + resp_frame.size()); |
| 533 | + wrapped[0] = static_cast<uint8_t>((resp_frame.size() >> 24) & 0xFF); |
| 534 | + wrapped[1] = static_cast<uint8_t>((resp_frame.size() >> 16) & 0xFF); |
| 535 | + wrapped[2] = static_cast<uint8_t>((resp_frame.size() >> 8) & 0xFF); |
| 536 | + wrapped[3] = static_cast<uint8_t>(resp_frame.size() & 0xFF); |
| 537 | + std::memcpy(wrapped.data() + 4, resp_frame.data(), resp_frame.size()); |
| 538 | + ::send(conn, reinterpret_cast<const char*>(wrapped.data()), |
| 539 | + static_cast<int>(wrapped.size()), 0); |
| 540 | + closesocket(conn); |
| 541 | + }); |
| 542 | + |
| 543 | + ClientConfig config; |
| 544 | + config.agent_addr = agent.address(); |
| 545 | + config.control_addr = "127.0.0.1:" + std::to_string(control_port); |
| 546 | + config.service_id = "cpp-manifest-test"; |
| 547 | + config.game_id = "game-test"; |
| 548 | + config.env = "development"; |
| 549 | + config.timeout_seconds = 5; |
| 550 | + config.disable_logging = true; |
| 551 | + |
| 552 | + CroupierClient client(config); |
| 553 | + FunctionDescriptor desc; |
| 554 | + desc.id = "player.ban"; |
| 555 | + desc.version = "1.0.0"; |
| 556 | + desc.input_schema = R"({"type":"object","properties":{"id":{"type":"string"}}})"; |
| 557 | + ASSERT_TRUE(client.RegisterFunction(desc, [](const std::string&, const std::string&) { |
| 558 | + return std::string("ok"); |
| 559 | + })); |
| 560 | + |
| 561 | + // Connect 内部:agent 握手成功后向控制面上传 manifest(best-effort) |
| 562 | + ASSERT_TRUE(client.Connect()); |
| 563 | + control_thread.join(); |
| 564 | + agent_thread.join(); |
| 565 | + client.Close(); |
| 566 | + closesocket(control_fd); |
| 567 | + ASSERT_TRUE(got_manifest.load()); |
| 568 | +} |
| 569 | + |
431 | 570 | } // namespace croupier::sdk::test |
432 | 571 |
|
433 | 572 | namespace croupier::sdk::test { |
|
0 commit comments