Files
msime-windows/server/tests/src/test_active_client_state.cpp
T
fanlusky 1794aa8741 fix(server): 切换到其他输入法时前台线程的停用不再因路由归属被丢弃
路由被 TextInputHost、任务栏或后挂起的线程占着时,前台应用切走输入法发来的
终止停用会因为不是路由所有者而被静默丢弃,g_is_ime_active 保持为 true,
悬浮工具栏不隐藏。现在来自前台窗口(或其焦点窗口)所在线程的终止停用会退役
残留路由并推进 epoch,照常投递 ClientDeactivated;后台线程仍被忽略。
2026-09-29 03:18:58 +08:00

158 lines
5.7 KiB
C++

#include "ipc/active_client_state.h"
#include "ipc/focus_session_policy.h"
#include "tests/includes/test_framework.h"
TEST_CASE(active_client_stale_deactivation_cannot_clear_new_active_client)
{
FanyImeIpc::ActiveClientState state;
const auto first_a = state.activate(1001);
REQUIRE(first_a.changed);
const auto b = state.activate(2002);
REQUIRE(b.changed);
const auto second_a = state.activate(1001);
REQUIRE(second_a.changed);
REQUIRE_EQ(state.deactivate(2002), 0u);
REQUIRE(state.matches(1001, second_a.epoch));
}
TEST_CASE(active_client_implicit_input_activation_is_idempotent)
{
FanyImeIpc::ActiveClientState state;
const auto first = state.activate(1001);
REQUIRE(first.changed);
const auto repeated = state.activate(1001);
REQUIRE(!repeated.changed);
REQUIRE_EQ(repeated.epoch, first.epoch);
state.activate(2002);
const auto recovered = state.activate(1001);
REQUIRE(recovered.changed);
REQUIRE(state.matches(1001, recovered.epoch));
}
TEST_CASE(active_client_old_epoch_tasks_are_rejected)
{
FanyImeIpc::ActiveClientState state;
const auto old_a = state.activate(1001);
state.activate(2002);
const auto current_a = state.activate(1001);
REQUIRE(!state.matches(1001, old_a.epoch));
REQUIRE(state.matches(1001, current_a.epoch));
}
TEST_CASE(active_client_same_client_reconnect_advances_epoch)
{
FanyImeIpc::ActiveClientState state;
const auto old_session = state.activate(1001);
REQUIRE(state.deactivate(1001) != 0);
const auto new_session = state.activate(1001);
REQUIRE(new_session.changed);
REQUIRE(old_session.epoch != new_session.epoch);
REQUIRE(!state.matches(1001, old_session.epoch));
REQUIRE(state.matches(1001, new_session.epoch));
}
TEST_CASE(active_client_transport_failure_invalidates_only_exact_epoch)
{
FanyImeIpc::ActiveClientState state;
const auto old_session = state.activate(1001);
REQUIRE(state.invalidate(1001, old_session.epoch) != 0);
REQUIRE(!state.is_current(1001));
const auto new_session = state.activate(1001);
REQUIRE_EQ(state.invalidate(1001, old_session.epoch), 0u);
REQUIRE(state.matches(1001, new_session.epoch));
}
TEST_CASE(active_client_new_focus_session_renews_same_client_epoch)
{
FanyImeIpc::ActiveClientState state;
const auto first_focus = state.activate(1001);
const auto second_focus = state.renew(1001);
REQUIRE(second_focus.changed);
REQUIRE(first_focus.epoch != second_focus.epoch);
REQUIRE(!state.matches(1001, first_focus.epoch));
REQUIRE(state.matches(1001, second_focus.epoch));
}
TEST_CASE(active_client_terminal_deactivation_reuses_only_current_inactive_epoch)
{
FanyImeIpc::ActiveClientState state;
state.activate(1001);
const uint64_t suspended_epoch = state.deactivate(1001);
REQUIRE(suspended_epoch != 0);
REQUIRE_EQ(state.terminal_deactivation_epoch(1001, suspended_epoch), suspended_epoch);
// A later real deactivation observes an already-suspended route. It must
// still carry the exact inactive epoch to the UI worker.
REQUIRE_EQ(state.deactivate(1001), 0u);
REQUIRE_EQ(state.terminal_deactivation_epoch(1001), suspended_epoch);
REQUIRE(state.matches(0, suspended_epoch));
// Once another activation wins, the old terminal event cannot hide it.
const auto active = state.activate(2002);
REQUIRE_EQ(state.terminal_deactivation_epoch(1001), 0u);
REQUIRE(!state.matches(0, suspended_epoch));
REQUIRE(state.matches(2002, active.epoch));
// Inactive state is not sufficient ownership: after B suspends, a delayed
// terminal event or disconnect from A must not reuse B's zero-state epoch.
const uint64_t second_suspended_epoch = state.deactivate(2002);
REQUIRE(second_suspended_epoch != 0);
REQUIRE_EQ(state.terminal_deactivation_epoch(1001), 0u);
REQUIRE_EQ(state.terminal_deactivation_epoch(2002), second_suspended_epoch);
}
TEST_CASE(active_client_foreground_terminal_deactivation_retires_foreign_route)
{
FanyImeIpc::ActiveClientState state;
// The foreground app suspended, then another client (e.g. TextInputHost)
// took routing and never gave it back. The app's terminal event is not
// routed to it, so ownership alone rejects it.
state.activate(1001);
state.deactivate(1001);
const auto foreign = state.activate(3003);
REQUIRE_EQ(state.deactivate(1001), 0u);
REQUIRE_EQ(state.terminal_deactivation_epoch(1001), 0u);
// Once the caller proved 1001 owns the foreground thread, the stale route
// is retired and the UI worker's inactive check accepts the new epoch.
const uint64_t retired_epoch = state.retire_for_terminal_deactivation(1001);
REQUIRE(retired_epoch != 0);
REQUIRE(retired_epoch != foreign.epoch);
REQUIRE(!state.matches(3003, foreign.epoch));
REQUIRE(state.matches(0, retired_epoch));
REQUIRE_EQ(state.snapshot().client_id, 0u);
// The duplicate event from Deactivate() reuses the same epoch instead of
// being dropped or advancing it again.
REQUIRE_EQ(state.terminal_deactivation_epoch(1001), retired_epoch);
REQUIRE_EQ(state.terminal_deactivation_epoch(3003), 0u);
// A later activation supersedes the retirement.
const auto next = state.activate(3003);
REQUIRE(!state.matches(0, retired_epoch));
REQUIRE(state.matches(3003, next.epoch));
REQUIRE_EQ(state.retire_for_terminal_deactivation(0), 0u);
}
TEST_CASE(focus_session_ready_requires_nonzero_client_epoch_and_token)
{
REQUIRE(FanyImeIpc::CanSendFocusSessionReady(1001, 22, 77));
REQUIRE(!FanyImeIpc::CanSendFocusSessionReady(0, 22, 77));
REQUIRE(!FanyImeIpc::CanSendFocusSessionReady(1001, 0, 77));
REQUIRE(!FanyImeIpc::CanSendFocusSessionReady(1001, 22, 0));
}