test: 커맨드 차등 스냅샷과 수명주기 계측을 보강
명령 상태의 자료형과 로그, 메시지, 순위 그래프를 보존하고 실제 명령 호출을 관찰하는 비교 전용 trace를 추가합니다. 계측은 CLI 및 명시적 차등 테스트 환경에서만 실행되며 제품 동작은 변경하지 않습니다.
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace sammo;
|
||||
|
||||
use sammo\API\Message\DecideMessageResponse;
|
||||
use sammo\Enums\MessageType;
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
if (getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') {
|
||||
fwrite(STDERR, "TURN_DIFFERENTIAL_ENABLED=1 is required\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
define('SAMMO_TURN_COMPARISON_LIBRARY_ONLY', true);
|
||||
require_once __DIR__ . '/turn_command_trace.php';
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
function comparisonInstantDiplomacyMessageRow(int $messageId): array
|
||||
{
|
||||
$row = DB::db()->queryFirstRow(
|
||||
'SELECT id, mailbox, type, src, dest, time, valid_until, message FROM message WHERE id = %i',
|
||||
$messageId,
|
||||
);
|
||||
if ($row === null) {
|
||||
throw new \RuntimeException("message {$messageId} was not persisted");
|
||||
}
|
||||
return [
|
||||
'id' => (int)$row['id'],
|
||||
'mailbox' => (int)$row['mailbox'],
|
||||
'type' => (string)$row['type'],
|
||||
'sourceId' => (int)$row['src'],
|
||||
'destinationId' => (int)$row['dest'],
|
||||
'timeTick' => (int)$row['time'],
|
||||
'validUntilTick' => (int)$row['valid_until'],
|
||||
'payload' => comparisonJsonValue($row['message']),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
function comparisonRunInstantDiplomacyResponse(array $request): array
|
||||
{
|
||||
$action = $request['action'] ?? null;
|
||||
if (!in_array($action, [
|
||||
DiplomaticMessage::TYPE_NO_AGGRESSION,
|
||||
DiplomaticMessage::TYPE_CANCEL_NA,
|
||||
DiplomaticMessage::TYPE_STOP_WAR,
|
||||
], true)) {
|
||||
throw new \InvalidArgumentException('action must be noAggression, cancelNA, or stopWar');
|
||||
}
|
||||
$actorGeneralId = $request['actorGeneralId'] ?? null;
|
||||
$proposerGeneralId = $request['proposerGeneralId'] ?? null;
|
||||
if (!is_int($actorGeneralId) || $actorGeneralId < 1) {
|
||||
throw new \InvalidArgumentException('actorGeneralId must be a positive integer');
|
||||
}
|
||||
if (!is_int($proposerGeneralId) || $proposerGeneralId < 1) {
|
||||
throw new \InvalidArgumentException('proposerGeneralId must be a positive integer');
|
||||
}
|
||||
if (($request['response'] ?? true) !== true) {
|
||||
throw new \InvalidArgumentException('this comparison runner currently covers accepted responses only');
|
||||
}
|
||||
|
||||
comparisonApplyTurnFixtureSetup($request['setup'] ?? null);
|
||||
$db = DB::db();
|
||||
$actor = $db->queryFirstRow(
|
||||
'SELECT no, name, nation FROM general WHERE no = %i',
|
||||
$actorGeneralId,
|
||||
);
|
||||
$proposer = $db->queryFirstRow(
|
||||
'SELECT no, name, nation FROM general WHERE no = %i',
|
||||
$proposerGeneralId,
|
||||
);
|
||||
if ($actor === null || $proposer === null) {
|
||||
throw new \RuntimeException('fixture generals are missing');
|
||||
}
|
||||
$actorNation = $db->queryFirstRow(
|
||||
'SELECT nation, name, color FROM nation WHERE nation = %i',
|
||||
(int)$actor['nation'],
|
||||
);
|
||||
$proposerNation = $db->queryFirstRow(
|
||||
'SELECT nation, name, color FROM nation WHERE nation = %i',
|
||||
(int)$proposer['nation'],
|
||||
);
|
||||
if ($actorNation === null || $proposerNation === null) {
|
||||
throw new \RuntimeException('fixture nations are missing');
|
||||
}
|
||||
|
||||
$messageBaseline = (int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM message') ?? 0);
|
||||
$logBaseline = (int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM general_record') ?? 0);
|
||||
$src = new MessageTarget(
|
||||
$proposerGeneralId,
|
||||
(string)$proposer['name'],
|
||||
(int)$proposerNation['nation'],
|
||||
(string)$proposerNation['name'],
|
||||
(string)$proposerNation['color'],
|
||||
);
|
||||
$dest = new MessageTarget(
|
||||
$actorGeneralId,
|
||||
(string)$actor['name'],
|
||||
(int)$actorNation['nation'],
|
||||
(string)$actorNation['name'],
|
||||
(string)$actorNation['color'],
|
||||
);
|
||||
$option = ['action' => $action];
|
||||
if ($action === DiplomaticMessage::TYPE_NO_AGGRESSION) {
|
||||
$year = $request['year'] ?? null;
|
||||
$month = $request['month'] ?? null;
|
||||
if (!is_int($year) || !is_int($month)) {
|
||||
throw new \InvalidArgumentException('noAggression requires integer year and month');
|
||||
}
|
||||
$option['year'] = $year;
|
||||
$option['month'] = $month;
|
||||
}
|
||||
$proposal = new DiplomaticMessage(
|
||||
MessageType::diplomacy,
|
||||
$src,
|
||||
$dest,
|
||||
'외교 제안',
|
||||
Message::gameNow(),
|
||||
new \DateTime('9999-12-31'),
|
||||
$option,
|
||||
);
|
||||
$proposalMessageId = $proposal->send();
|
||||
$proposalBefore = comparisonInstantDiplomacyMessageRow($proposalMessageId);
|
||||
|
||||
$observe = $request['observe'] ?? [];
|
||||
if (!is_array($observe)) {
|
||||
throw new \InvalidArgumentException('observe must be an object');
|
||||
}
|
||||
$observe['logAfterId'] = $logBaseline;
|
||||
$observe['messageAfterId'] = $messageBaseline;
|
||||
$snapshotRequest = ['observe' => $observe];
|
||||
$before = comparisonTurnStateSnapshot($snapshotRequest);
|
||||
|
||||
$session = Session::getInstance();
|
||||
$_SESSION[UniqueConst::$serverID . Session::GAME_KEY_GENERAL_ID] = $actorGeneralId;
|
||||
$_SESSION[UniqueConst::$serverID . Session::GAME_KEY_GENERAL_NAME] = (string)$actor['name'];
|
||||
$api = new DecideMessageResponse(dirname(__DIR__), [
|
||||
'msgID' => $proposalMessageId,
|
||||
'response' => true,
|
||||
]);
|
||||
$validationError = $api->validateArgs();
|
||||
if ($validationError !== null) {
|
||||
throw new \RuntimeException("DecideMessageResponse validation failed: {$validationError}");
|
||||
}
|
||||
$apiResult = $api->launch($session, null, null);
|
||||
if (!is_array($apiResult)) {
|
||||
throw new \RuntimeException('DecideMessageResponse did not return an array result');
|
||||
}
|
||||
|
||||
return [
|
||||
'schemaVersion' => 1,
|
||||
'engine' => 'ref',
|
||||
'execution' => [
|
||||
'kind' => 'instantDiplomacyMessageResponse',
|
||||
'entryPoint' => DecideMessageResponse::class,
|
||||
'actorGeneralId' => $actorGeneralId,
|
||||
'proposerGeneralId' => $proposerGeneralId,
|
||||
'action' => $action,
|
||||
'response' => true,
|
||||
'outcome' => $apiResult,
|
||||
'proposalMessageId' => $proposalMessageId,
|
||||
'proposalBefore' => $proposalBefore,
|
||||
'proposalAfter' => comparisonInstantDiplomacyMessageRow($proposalMessageId),
|
||||
],
|
||||
'before' => $before,
|
||||
'after' => comparisonTurnStateSnapshot($snapshotRequest),
|
||||
'rng' => [],
|
||||
];
|
||||
}
|
||||
|
||||
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(
|
||||
comparisonRunInstantDiplomacyResponse($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);
|
||||
}
|
||||
Reference in New Issue
Block a user