test: 커맨드 차등 스냅샷과 수명주기 계측을 보강
명령 상태의 자료형과 로그, 메시지, 순위 그래프를 보존하고 실제 명령 호출을 관찰하는 비교 전용 trace를 추가합니다. 계측은 CLI 및 명시적 차등 테스트 환경에서만 실행되며 제품 동작은 변경하지 않습니다.
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user