test: 커맨드 차등 스냅샷과 수명주기 계측을 보강

명령 상태의 자료형과 로그, 메시지, 순위 그래프를 보존하고 실제 명령 호출을 관찰하는 비교 전용 trace를 추가합니다.

계측은 CLI 및 명시적 차등 테스트 환경에서만 실행되며 제품 동작은 변경하지 않습니다.
This commit is contained in:
2026-08-23 21:49:10 +00:00
parent e2033637e3
commit a041a8132f
4 changed files with 814 additions and 21 deletions
+78 -15
View File
@@ -99,6 +99,10 @@ function comparisonApplyTurnFixtureSetup(mixed $setup): void
if (!is_array($world)) {
throw new \InvalidArgumentException('setup.world must be an object');
}
$freezeClock = $world['freezeClock'] ?? false;
if (!is_bool($freezeClock)) {
throw new \InvalidArgumentException('setup.world.freezeClock must be a boolean');
}
$game = KVStorage::getStorage($db, 'game_env');
foreach (['year', 'month', 'startyear', 'init_year', 'init_month'] as $key) {
$fixtureKey = match ($key) {
@@ -160,6 +164,12 @@ function comparisonApplyTurnFixtureSetup(mixed $setup): void
}
GameConst::$staticEventHandlers = $handlersByEvent;
}
if ($freezeClock) {
$clock = GameClock::fromStorage($game);
$frozenTick = $clock->nowTick();
$clock->persistTick($game, $frozenTick, GameClock::MODE_MANUAL);
$game->resetCache();
}
}
foreach (['nations', 'cities', 'generals', 'troops', 'diplomacy', 'generalTurns', 'nationTurns'] as $collection) {
@@ -440,6 +450,8 @@ function comparisonApplyTurnFixtureSetup(mixed $setup): void
'killTurn' => 'killturn',
'npcState' => 'npc',
'blockState' => 'block',
'picture' => 'picture',
'imageServer' => 'imgsvr',
'specialDomestic' => 'special',
'specialWar' => 'special2',
'personality' => 'personal',
@@ -592,6 +604,43 @@ function comparisonApplyTurnFixtureSetup(mixed $setup): void
}
}
/** @return array{generalIds: list<int>, cityIds: list<int>, nationIds: list<int>, troopIds: list<int>} */
function comparisonTurnCommandEntityIds(): array
{
$db = DB::db();
return [
'generalIds' => array_map('intval', $db->queryFirstColumn('SELECT no FROM general ORDER BY no')),
'cityIds' => array_map('intval', $db->queryFirstColumn('SELECT city FROM city ORDER BY city')),
'nationIds' => array_map('intval', $db->queryFirstColumn('SELECT nation FROM nation ORDER BY nation')),
'troopIds' => array_map('intval', $db->queryFirstColumn('SELECT troop_leader FROM troop ORDER BY troop_leader')),
];
}
/**
* Extend an explicit selector with entities created by the command. Without
* this closure, a successful founding/recruitment can remain absent from both
* snapshots and therefore produce a false green differential.
*
* @param array{observe?: mixed} $snapshotRequest
* @param array{generalIds: list<int>, cityIds: list<int>, nationIds: list<int>, troopIds: list<int>} $before
* @return array{observe: array<string, mixed>}
*/
function comparisonCloseSnapshotRequestOverCreatedEntities(array $snapshotRequest, array $before): array
{
$observe = $snapshotRequest['observe'] ?? [];
if (!is_array($observe)) {
throw new \InvalidArgumentException('observe must be an object');
}
$after = comparisonTurnCommandEntityIds();
foreach (['generalIds', 'cityIds', 'nationIds', 'troopIds'] as $selector) {
$selected = comparisonIntegerList($observe[$selector] ?? [], $selector);
$created = array_values(array_diff($after[$selector], $before[$selector]));
$observe[$selector] = array_values(array_unique([...$selected, ...$created]));
sort($observe[$selector], SORT_NUMERIC);
}
return ['observe' => $observe];
}
function comparisonRunTurnCommand(array $request): array
{
if (getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') {
@@ -662,6 +711,7 @@ function comparisonRunTurnCommand(array $request): array
}
}
$snapshotRequest = ['observe' => $request['observe'] ?? []];
$beforeEntityIds = comparisonTurnCommandEntityIds();
$before = comparisonTurnStateSnapshot($snapshotRequest);
$db = DB::db();
$gameStorage = KVStorage::getStorage($db, 'game_env');
@@ -705,7 +755,9 @@ function comparisonRunTurnCommand(array $request): array
$general->setVar('troop', $troopId);
StaticEventHandler::handleEvent($general, null, \sammo\API\Troop\JoinTroop::class, [], $args);
$general->applyDB($db);
$after = comparisonTurnStateSnapshot($snapshotRequest);
$after = comparisonTurnStateSnapshot(
comparisonCloseSnapshotRequestOverCreatedEntities($snapshotRequest, $beforeEntityIds),
);
return [
'schemaVersion' => 1,
@@ -732,7 +784,9 @@ function comparisonRunTurnCommand(array $request): array
$completed = $command->run(NoRNG::rngInstance());
$command->setNextAvailable();
$general->getLogger()->flush();
$after = comparisonTurnStateSnapshot($snapshotRequest);
$after = comparisonTurnStateSnapshot(
comparisonCloseSnapshotRequestOverCreatedEntities($snapshotRequest, $beforeEntityIds),
);
return [
'schemaVersion' => 1,
@@ -816,7 +870,9 @@ function comparisonRunTurnCommand(array $request): array
$general->getLogger()->flush();
$turn->applyDB();
unset($turn);
$after = comparisonTurnStateSnapshot($snapshotRequest);
$after = comparisonTurnStateSnapshot(
comparisonCloseSnapshotRequestOverCreatedEntities($snapshotRequest, $beforeEntityIds),
);
$resultTurnRaw = $resultTurn->toRaw();
$resultTerm = (int)($resultTurnRaw['term'] ?? 0);
$preReqTurn = $command->getPreReqTurn();
@@ -853,6 +909,9 @@ function comparisonRunTurnCommand(array $request): array
return [
'schemaVersion' => 1,
'engine' => 'ref',
'harness' => [
'messageSharedIconBaseUrl' => ServConfig::getSharedIconPath(),
],
'execution' => [
'kind' => $kind,
'actorGeneralId' => $actorGeneralId,
@@ -871,17 +930,21 @@ function comparisonRunTurnCommand(array $request): array
];
}
try {
$input = stream_get_contents(STDIN);
$request = json_decode($input === '' ? '{}' : $input, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($request)) {
throw new \InvalidArgumentException('request must be an object');
if (!defined('SAMMO_TURN_COMPARISON_LIBRARY_ONLY')) {
try {
$input = stream_get_contents(STDIN);
$request = json_decode($input === '' ? '{}' : $input, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($request)) {
throw new \InvalidArgumentException('request must be an object');
}
echo json_encode(
comparisonRunTurnCommand($request),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
), PHP_EOL;
} catch (\Throwable $throwable) {
fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
exit(1);
}
echo json_encode(
comparisonRunTurnCommand($request),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
), PHP_EOL;
} catch (\Throwable $throwable) {
fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
exit(1);
} elseif (getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') {
throw new \RuntimeException('TURN_DIFFERENTIAL_ENABLED=1 is required for comparison library mode');
}