2V1-e: live login smoke harness (port.login_live)
port_login_flow_test gains a `live` mode that runs the same script path against a real 40250 server and checks the client's own state: the character list, then LoadingWindow.LoadData from GC_MAIN_CHARACTER (which CPythonNetworkStream answers with CG_CLIENT_VERSION), exiting before the game phase. Host, ports, account and slot come only from MT_LIVE_* env vars, are handed to Python as objects and never printed; without them the test skips. Not yet run live: the server was unreachable today. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8d9d863c7e
commit
2a8750e736
@@ -219,6 +219,10 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
|
||||
add_test(NAME port.login_flow
|
||||
COMMAND $<TARGET_FILE:port_login_flow_test> ${MT_40250_CLIENT} ${MT_PYTHON_STDLIB_ZIP})
|
||||
set_tests_properties(port.login_flow PROPERTIES SKIP_RETURN_CODE 77)
|
||||
# 2V1-e: the same path against a live 40250 server; skipped unless MT_LIVE_* is set (see the test).
|
||||
add_test(NAME port.login_live
|
||||
COMMAND $<TARGET_FILE:port_login_flow_test> ${MT_40250_CLIENT} ${MT_PYTHON_STDLIB_ZIP} live)
|
||||
set_tests_properties(port.login_live PROPERTIES SKIP_RETURN_CODE 77 RUN_SERIAL TRUE)
|
||||
|
||||
add_executable(port_python_ui_modules_test ${CMAKE_CURRENT_SOURCE_DIR}/../../tests/port_python_ui_modules_test.cpp)
|
||||
target_link_libraries(port_python_ui_modules_test PRIVATE port_platform)
|
||||
|
||||
@@ -18,7 +18,16 @@
|
||||
// The server checks every sequence byte against the 40250 table, so a skipped or extra sequenced
|
||||
// packet fails the run.
|
||||
//
|
||||
// port_login_flow_test <40250 Client dir> <python27.zip>
|
||||
// port_login_flow_test <40250 Client dir> <python27.zip> [live]
|
||||
//
|
||||
// With `live` (批次 2V1-e, ctest port.login_live) the same path runs against a real 40250 server and
|
||||
// the client's own state is the proof: the character list arrived, and the loading window got
|
||||
// LoadData from GC_MAIN_CHARACTER, which CPythonNetworkStream::RecvMainCharacter answers with
|
||||
// CG_CLIENT_VERSION. The test exits before net.StartGame() would enter the game. Everything about the
|
||||
// server comes from the environment and nothing of it is printed:
|
||||
//
|
||||
// MT_LIVE_HOST, MT_LIVE_LOGIN, MT_LIVE_PASSWORD required; without them the test is skipped
|
||||
// MT_LIVE_AUTH_PORT (11000), MT_LIVE_GAME_PORT (13000, channel 1), MT_LIVE_SLOT (0)
|
||||
//
|
||||
// Exit 77 (ctest SKIP) when the client is missing, unless MT_ASSETS_STRICT=1, which fails instead.
|
||||
#include "ScriptLib/StdAfx.h"
|
||||
@@ -27,12 +36,15 @@
|
||||
#include "../src/platform/UserInterface/UserInterface.h"
|
||||
#include "port_login_flow_server.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -91,24 +103,62 @@ static void dump(FakeLoginServer& server)
|
||||
std::fprintf(stderr, "\nserver error: %s\n", server.Error().c_str());
|
||||
}
|
||||
|
||||
static std::string env(const char* name, const char* fallback = "")
|
||||
{
|
||||
const char* value = std::getenv(name);
|
||||
return value && *value ? value : fallback;
|
||||
}
|
||||
|
||||
// Hands a C string to __main__ as a Python object, so account data never goes through source text.
|
||||
static void py_set(const char* name, const std::string& value)
|
||||
{
|
||||
PyObject* object = PyString_FromString(value.c_str());
|
||||
PyDict_SetItemString(PyModule_GetDict(PyImport_AddModule((char*) "__main__")), name, object);
|
||||
Py_DECREF(object);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
const char* strict = std::getenv("MT_ASSETS_STRICT");
|
||||
const bool is_strict = strict && std::string(strict) == "1";
|
||||
const std::string client = argc > 1 ? argv[1] : "";
|
||||
const std::string stdlib = argc > 2 ? argv[2] : PythonHost::DefaultStdLibPath();
|
||||
const bool live = argc > 3 && std::string(argv[3]) == "live";
|
||||
if (client.empty() || chdir(client.c_str()) != 0 || access("pack/Index", 0) != 0)
|
||||
{
|
||||
std::fprintf(stderr, "port_login_flow_test: no 40250 Client/pack at '%s'\n", client.c_str());
|
||||
return is_strict ? 1 : 77;
|
||||
}
|
||||
|
||||
FakeLoginServer server;
|
||||
if (!server.Start())
|
||||
std::string host = "127.0.0.1", login = FakeLoginServer::kLogin, password = FakeLoginServer::kPassword;
|
||||
int auth_port = 0, game_port = 0, slot = 0;
|
||||
std::unique_ptr<FakeLoginServer> server;
|
||||
if (live)
|
||||
{
|
||||
std::fprintf(stderr, "port_login_flow_test: cannot listen on loopback\n");
|
||||
return 1;
|
||||
host = env("MT_LIVE_HOST");
|
||||
login = env("MT_LIVE_LOGIN");
|
||||
password = env("MT_LIVE_PASSWORD");
|
||||
if (host.empty() || login.empty() || password.empty())
|
||||
{
|
||||
std::fprintf(stderr, "port_login_flow_test: live mode needs MT_LIVE_HOST, MT_LIVE_LOGIN, MT_LIVE_PASSWORD\n");
|
||||
return 77;
|
||||
}
|
||||
auth_port = std::atoi(env("MT_LIVE_AUTH_PORT", "11000").c_str());
|
||||
game_port = std::atoi(env("MT_LIVE_GAME_PORT", "13000").c_str());
|
||||
slot = std::atoi(env("MT_LIVE_SLOT", "0").c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
server = std::make_unique<FakeLoginServer>();
|
||||
if (!server->Start())
|
||||
{
|
||||
std::fprintf(stderr, "port_login_flow_test: cannot listen on loopback\n");
|
||||
return 1;
|
||||
}
|
||||
auth_port = server->AuthPort();
|
||||
game_port = server->GamePort();
|
||||
}
|
||||
auto failed = [&] { return server && server->Failed(); };
|
||||
|
||||
PackSingletons();
|
||||
CHECK(PackInitialize("pack"));
|
||||
@@ -125,63 +175,95 @@ int main(int argc, char** argv)
|
||||
"__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"));
|
||||
CHECK(pump_until(20, [] { return py_true("isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"); }));
|
||||
|
||||
// 2. The login button's path, with the fake server for both the game and the auth address.
|
||||
char connect[256];
|
||||
std::snprintf(connect, sizeof(connect),
|
||||
"_stream.SetConnectInfo('127.0.0.1', %d, '127.0.0.1', %d)\n"
|
||||
"_stream.curPhaseWindow.Connect('%s', '%s')",
|
||||
server.GamePort(), server.AuthPort(), FakeLoginServer::kLogin, FakeLoginServer::kPassword);
|
||||
CHECK(py(connect));
|
||||
// 2. The login button's path (LoginWindow.Connect), pointed at the fake or the live server.
|
||||
py_set("_host", host);
|
||||
py_set("_login", login);
|
||||
py_set("_password", password);
|
||||
char ports[128];
|
||||
std::snprintf(ports, sizeof(ports), "_auth_port, _game_port, _slot = %d, %d, %d", auth_port, game_port, slot);
|
||||
CHECK(py(ports));
|
||||
CHECK(py("_stream.SetConnectInfo(_host, _game_port, _host, _auth_port)\n"
|
||||
"_stream.curPhaseWindow.Connect(_login, _password)\n"
|
||||
"del __main__._login, __main__._password"));
|
||||
|
||||
// 3. The character list.
|
||||
const bool selecting = pump_until(20, [&] {
|
||||
return server.Failed() ||
|
||||
(server.Has("game1:login2") && py_true("isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"));
|
||||
const bool selecting = pump_until(live ? 30 : 20, [&] {
|
||||
return failed() || ((!server || server->Has("game1:login2")) &&
|
||||
py_true("isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)"));
|
||||
});
|
||||
CHECK(selecting && !server.Failed());
|
||||
CHECK(server.Has("auth:login3") && server.Has("auth:closed"));
|
||||
CHECK(py("import net\n"
|
||||
"_name = net.GetAccountCharacterSlotDataString(0, net.ACCOUNT_CHARACTER_SLOT_NAME)\n"
|
||||
"assert _name == '" + std::string(FakeLoginServer::kCharacterName) + "', _name\n"
|
||||
"assert net.GetEmpireID() == 1"));
|
||||
|
||||
// 4-5. Start the game on slot 0: DirectEnter, the second game login and the loading phase.
|
||||
CHECK(py("_stream.curPhaseWindow.StartGame()"));
|
||||
const bool loading = pump_until(30, [&] { return server.Failed() || server.Has("game2:client_version"); });
|
||||
CHECK(loading && !server.Failed());
|
||||
// Checked right away: introLoading's own update steps end in net.StartGame(), after which the stream
|
||||
// opens game.GameWindow itself (CPythonNetworkStream::Process, m_isStartGame), which is 2V2.
|
||||
CHECK(py_true("isinstance(_stream.curPhaseWindow, introLoading.LoadingWindow)"));
|
||||
|
||||
const std::vector<std::string> expected = {
|
||||
"auth:handshake", "auth:login3", "game1:handshake", "game1:login2", "auth:closed",
|
||||
"game1:closed", "game2:handshake", "game2:login2", "game2:select", "game2:client_version",
|
||||
};
|
||||
auto events = server.Events();
|
||||
// auth:closed races game1's handshake; compare the order within each connection.
|
||||
for (const char* prefix : {"auth:", "game1:", "game2:"})
|
||||
CHECK(selecting && !failed());
|
||||
if (!selecting)
|
||||
{
|
||||
std::vector<std::string> want, got;
|
||||
for (const auto& e : expected)
|
||||
if (e.rfind(prefix, 0) == 0)
|
||||
want.push_back(e);
|
||||
for (const auto& e : events)
|
||||
if (e.rfind(prefix, 0) == 0)
|
||||
got.push_back(e);
|
||||
CHECK(want == got);
|
||||
// The login window reports failures in its popup (LOGIN_FAILURE, connect errors).
|
||||
py("import sys\n"
|
||||
"_w = _stream.curPhaseWindow\n"
|
||||
"_msg = _stream.popupWindow and _stream.popupWindow.IsShow() and _stream.popupWindow.GetChild('message').GetText()\n"
|
||||
"sys.stderr.write('phase window %s, popup %r\\n' % (_w.__class__.__name__, _msg))");
|
||||
}
|
||||
if (server)
|
||||
{
|
||||
CHECK(server->Has("auth:login3") && server->Has("auth:closed"));
|
||||
CHECK(py("import net\n"
|
||||
"_name = net.GetAccountCharacterSlotDataString(0, net.ACCOUNT_CHARACTER_SLOT_NAME)\n"
|
||||
"assert _name == '" + std::string(FakeLoginServer::kCharacterName) + "', _name\n"
|
||||
"assert net.GetEmpireID() == 1"));
|
||||
}
|
||||
else if (selecting)
|
||||
{
|
||||
CHECK(py("import net\n"
|
||||
"assert net.GetAccountCharacterSlotDataString(_slot, net.ACCOUNT_CHARACTER_SLOT_NAME), 'slot is empty'\n"
|
||||
"assert net.GetEmpireID() in (1, 2, 3), net.GetEmpireID()"));
|
||||
}
|
||||
|
||||
// 4-5. Start the game on the slot: DirectEnter, the second game login and the loading phase.
|
||||
if (selecting)
|
||||
{
|
||||
CHECK(py("_stream.curPhaseWindow.SelectSlot(_slot)\n_stream.curPhaseWindow.StartGame()"));
|
||||
// GC_MAIN_CHARACTER → LoadingWindow.LoadData(x, y), then CG_CLIENT_VERSION in the same call.
|
||||
const char* loaded = "isinstance(_stream.curPhaseWindow, introLoading.LoadingWindow) and "
|
||||
"getattr(_stream.curPhaseWindow, 'playerX', None) is not None";
|
||||
const bool loading = pump_until(live ? 40 : 30, [&] {
|
||||
return failed() || (server ? server->Has("game2:client_version") : py_true(loaded));
|
||||
});
|
||||
CHECK(loading && !failed());
|
||||
// Checked right away: introLoading's own update steps end in net.StartGame(), after which the stream
|
||||
// opens game.GameWindow itself (CPythonNetworkStream::Process, m_isStartGame), which is 2V2.
|
||||
CHECK(py_true(loaded));
|
||||
}
|
||||
|
||||
if (server)
|
||||
{
|
||||
const std::vector<std::string> expected = {
|
||||
"auth:handshake", "auth:login3", "game1:handshake", "game1:login2", "auth:closed",
|
||||
"game1:closed", "game2:handshake", "game2:login2", "game2:select", "game2:client_version",
|
||||
};
|
||||
auto events = server->Events();
|
||||
// auth:closed races game1's handshake; compare the order within each connection.
|
||||
for (const char* prefix : {"auth:", "game1:", "game2:"})
|
||||
{
|
||||
std::vector<std::string> want, got;
|
||||
for (const auto& e : expected)
|
||||
if (e.rfind(prefix, 0) == 0)
|
||||
want.push_back(e);
|
||||
for (const auto& e : events)
|
||||
if (e.rfind(prefix, 0) == 0)
|
||||
got.push_back(e);
|
||||
CHECK(want == got);
|
||||
}
|
||||
if (g_failures)
|
||||
dump(*server);
|
||||
}
|
||||
if (g_failures)
|
||||
dump(server);
|
||||
|
||||
CHECK(py("import app\napp.Exit()"));
|
||||
pump_until(2, [] { return !PythonBoot::IsAppLooping(); });
|
||||
CHECK(!PythonBoot::IsAppLooping());
|
||||
PythonBoot::Stop();
|
||||
server.Stop();
|
||||
if (server)
|
||||
server->Stop();
|
||||
|
||||
if (g_failures)
|
||||
std::fprintf(stderr, "%d check(s) failed\n", g_failures);
|
||||
else
|
||||
std::printf("port_login_flow_test: ok\n");
|
||||
std::printf("port_login_flow_test: ok%s\n", live ? " (live)" : "");
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user