test(net): 去掉 net 测试的 flaky,顺带把整套 ctest 从 78.7s 降到 8.7s
全量 ctest 连续两次各挂一个 net 测试,且挂的不是同一个,压测后定位到
四个互相独立的原因,全部在测试自身,产品代码未改动。
1. accept_loopback(): poll() 的 `<= 0` 把 EINTR 当成对端从未连接,且单片
500ms 超时在有负载时不够。50 轮压测里 4 次失败全部源于此。改为 EINTR
重试 + 5s 截止时间(listener 本就非阻塞,超时后 accept 不会挂住)。
2. :570 的 pump 循环条件抄自上面的死端口用例,但此处 connect 会成功,
stage 先经过 LoggingIn,循环提前一步退出。
3. 同文件 5 处手写 pump 循环只给 10-20ms 等真实 loopback socket。新增
pump_until(session, want, max_ms=2000) 统一替换,等的是断言真正关心
的那个状态。
4. net_loopback_test 的 10 处 run(..., N, []{return false;}) 是固定空转
而非等待条件。服务端批量收包改用该批最后一个包的标志作条件(TCP 有序),
客户端事件用本地累加器在谓词里 drain。
提速:两处 this-*-host-must-not-resolve.invalid 换成带空标签的名字。本机
解析器不按 RFC 6761 本地拒绝 .invalid,每次发往上游等满 30s 超时;空标签
让 getaddrinfo 在语法层返回同一个 EAI_NONAME,实测 <10ms。
net_classic_session_test 61.2s -> 3.5s;loopback 改条件退出后 0.94s。
验证:net_classic_session_test 100/100(修复前 50 轮挂 4 轮);
net_loopback_test 100/100(修复前单独跑第 7 轮即挂);全量 ctest 27/27;
port_map.py check 0 error;8 个 pack key 值均未入库。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -760,7 +760,10 @@ int main() {
|
||||
|
||||
// --- CG_CHANGE_NAME -> GC_CHANGE_NAME updates the local slot list ---
|
||||
CHECK(game.change_name(0, "Renamed"), "game: change_name sent");
|
||||
run(srv, game, 3000, [] { return false; });
|
||||
run(srv, game, 3000, [&] {
|
||||
return srv.m_change_name_seen && game.chars().size() == 1 &&
|
||||
game.chars()[0].name == "Renamed";
|
||||
});
|
||||
CHECK(srv.m_change_name_seen && srv.m_last_change_name.index == 0,
|
||||
"game: server got CG_CHANGE_NAME idx=0");
|
||||
CHECK(std::string(srv.m_last_change_name.name) == "Renamed",
|
||||
@@ -853,41 +856,65 @@ int main() {
|
||||
run(srv, game, 5000, [&] { return game.in_game(); });
|
||||
CHECK(game.in_game(), "game: reached PHASE_GAME");
|
||||
CHECK(srv.m_selected == 0, "game: server saw our selection");
|
||||
run(srv, game, 200, [] { return false; });
|
||||
CHECK(game.drain_guild_make_requests() == 1,
|
||||
int guild_make_requests = 0;
|
||||
run(srv, game, 3000, [&] {
|
||||
guild_make_requests += game.drain_guild_make_requests();
|
||||
return guild_make_requests >= 1;
|
||||
});
|
||||
CHECK(guild_make_requests == 1,
|
||||
"game: GC_REQUEST_MAKE_GUILD surfaced as an event");
|
||||
CHECK(game.send_warp(), "game: CG_WARP sent");
|
||||
run(srv, game, 200, [] { return false; });
|
||||
decltype(game.world().drain_warps()) warp_cues;
|
||||
run(srv, game, 3000, [&] {
|
||||
auto v = game.world().drain_warps();
|
||||
warp_cues.insert(warp_cues.end(), v.begin(), v.end());
|
||||
return srv.m_warp_seen && !warp_cues.empty();
|
||||
});
|
||||
CHECK(srv.m_warp_seen, "game: server got CG_WARP");
|
||||
{
|
||||
auto warps = game.world().drain_warps();
|
||||
auto &warps = warp_cues;
|
||||
CHECK(!warps.empty() && warps.back().x == 4321 && warps.back().y == 8765,
|
||||
"game: GC_WARP destination surfaced");
|
||||
CHECK(game.world().get(9902) == nullptr,
|
||||
"game: GC_WARP drops pipelined old-socket tail");
|
||||
}
|
||||
CHECK(game.send_fishing(36), "game: CG_FISHING sent");
|
||||
run(srv, game, 200, [] { return false; });
|
||||
decltype(game.world().drain_fishing_events()) fishing_events;
|
||||
run(srv, game, 3000, [&] {
|
||||
auto v = game.world().drain_fishing_events();
|
||||
fishing_events.insert(fishing_events.end(), v.begin(), v.end());
|
||||
return srv.m_fishing_seen && !fishing_events.empty();
|
||||
});
|
||||
CHECK(srv.m_fishing_seen && srv.m_last_fishing_dir == 36,
|
||||
"game: server got CG_FISHING direction");
|
||||
{
|
||||
auto fishing = game.world().drain_fishing_events();
|
||||
auto &fishing = fishing_events;
|
||||
CHECK(!fishing.empty() && fishing.back().subheader == FISHING_START &&
|
||||
fishing.back().info == 1000 && fishing.back().dir == 36,
|
||||
"game: GC_FISHING event surfaced");
|
||||
}
|
||||
CHECK(game.send_dungeon(), "game: CG_DUNGEON sent");
|
||||
run(srv, game, 200, [] { return false; });
|
||||
decltype(game.world().drain_dungeon_events()) dungeon_events;
|
||||
run(srv, game, 3000, [&] {
|
||||
auto v = game.world().drain_dungeon_events();
|
||||
dungeon_events.insert(dungeon_events.end(), v.begin(), v.end());
|
||||
return srv.m_dungeon_seen && !dungeon_events.empty();
|
||||
});
|
||||
CHECK(srv.m_dungeon_seen, "game: server got CG_DUNGEON");
|
||||
{
|
||||
auto dungeons = game.world().drain_dungeon_events();
|
||||
auto &dungeons = dungeon_events;
|
||||
CHECK(!dungeons.empty() && dungeons.back().subheader == DUNGEON_DESTINATION_POSITION &&
|
||||
dungeons.back().has_destination && dungeons.back().x == 3210 && dungeons.back().y == 6540,
|
||||
"game: GC_DUNGEON destination surfaced");
|
||||
}
|
||||
|
||||
// game-phase packets flowed into the EntityStore
|
||||
run(srv, game, 200, [] { return false; }); // drain a few more polls
|
||||
// The game-phase burst is what these checks read; wait for its last field
|
||||
// instead of hoping a fixed number of polls was enough.
|
||||
run(srv, game, 3000, [&] {
|
||||
return game.world().main_vid() == 1000 && game.world().get(1000) != nullptr &&
|
||||
srv.m_client_version_seen;
|
||||
});
|
||||
mtnet::EntityStore &wm = game.world();
|
||||
const EntityStore &w = wm;
|
||||
CHECK(w.main_vid() == 1000, "world: main vid from GC_MAIN_CHARACTER");
|
||||
@@ -926,7 +953,7 @@ int main() {
|
||||
CHECK(game.send_script_select_item(/*selection=*/91), "send_script_select_item");
|
||||
CHECK(game.send_quest_cancel(), "send_quest_cancel");
|
||||
CHECK(game.send_chat(1, "hello"), "send_chat");
|
||||
run(srv, game, 300, [] { return false; });
|
||||
run(srv, game, 3000, [&] { return srv.m_last_chat == "hello"; });
|
||||
CHECK(srv.m_move_count == 1 && srv.m_last_move.func == FUNC_MOVE &&
|
||||
srv.m_last_move.rot == 18 && srv.m_last_move.x == 12345 &&
|
||||
srv.m_last_move.y == 67890 && srv.m_last_move.time == 111,
|
||||
@@ -976,7 +1003,7 @@ int main() {
|
||||
CHECK(game.send_item_use_to_item(WINDOW_INVENTORY, 14, WINDOW_EQUIPMENT, 90), "send_item_use_to_item");
|
||||
CHECK(game.send_give_item(2000, WINDOW_INVENTORY, 15, 2), "send_give_item");
|
||||
CHECK(game.send_item_pickup(5000), "send_item_pickup");
|
||||
run(srv, game, 300, [] { return false; });
|
||||
run(srv, game, 3000, [&] { return srv.m_last_pickup_vid == 5000; });
|
||||
CHECK(srv.m_item_move_count == 1 && srv.m_last_item_move.pos.window_type == WINDOW_INVENTORY
|
||||
&& srv.m_last_item_move.pos.cell == 5
|
||||
&& srv.m_last_item_move.change_pos.window_type == WINDOW_EQUIPMENT
|
||||
@@ -1001,7 +1028,7 @@ int main() {
|
||||
CHECK(game.send_shop_buy(/*pos=*/3, /*count=*/2), "send_shop_buy");
|
||||
CHECK(game.send_exchange_start(2000), "send_exchange_start");
|
||||
CHECK(game.send_safebox_checkin(/*safe_pos=*/7, WINDOW_INVENTORY, 9), "send_safebox_checkin");
|
||||
run(srv, game, 300, [] { return false; });
|
||||
run(srv, game, 3000, [&] { return srv.m_last_safebox_checkin_pos == 7; });
|
||||
CHECK(srv.m_last_party_invite_vid == 2000, "server got CG_PARTY_INVITE{2000}");
|
||||
CHECK(srv.m_party_skill_count == 1 && srv.m_last_party_skill.skill_index == 4 &&
|
||||
srv.m_last_party_skill.target_vid == 2000,
|
||||
@@ -1036,7 +1063,7 @@ int main() {
|
||||
shop_items.push_back(si);
|
||||
CHECK(game.send_open_private_shop("Cheap swords", shop_items), "send_open_private_shop");
|
||||
|
||||
run(srv, game, 300, [] { return false; });
|
||||
run(srv, game, 3000, [&] { return srv.m_myshop_seen; });
|
||||
CHECK(srv.m_mall_checkout_seen && srv.m_last_mall_checkout.mall_pos == 0 &&
|
||||
srv.m_last_mall_checkout.inv_pos.cell == 11,
|
||||
"server got CG_MALL_CHECKOUT{mall=0,inv=11}");
|
||||
|
||||
Reference in New Issue
Block a user