Files
SGF/scripts/benchmarks/ModuleAssociationReadWorkerTest.php
2026-11-02 20:29:34 +02:00

98 lines
7.0 KiB
PHP

<?php
namespace Tests\Benchmarks;
use App\Models\User;
use App\Modules\Identity\Services\ModuleAssociationReview;
use App\Shared\Context\CompanyContext;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
// Test subprocess only, outside the public root and normal test discovery.
final class ModuleAssociationReadWorkerTest extends TestCase
{
public function test_isolated_worker(): void
{
$run = (string) getenv('SGF_ISOLATED_REGRESSION_RUN');
$this->assertMatchesRegularExpression('/^[a-f0-9]{12}$/D', $run);
$file = realpath((string) getenv('SGF_ASSOCIATION_WORKER_INPUT'));
$private = realpath(base_path('../private'));
if (!$file || !$private || !str_starts_with($file, $private.DIRECTORY_SEPARATOR)) throw new \RuntimeException('Unexpected private input');
$p = json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR);
$identity = DB::selectOne("SELECT current_database() AS database,current_user AS role,inet_server_port() AS port,current_setting('cluster_name') AS cluster");
$this->assertSame('sgf_tempo_test', $identity->database); $this->assertSame('sgf_test_runner', $identity->role);
$this->assertSame('sgf_modular_regression_'.$run, $identity->cluster); $this->assertSame(55439, (int) $identity->port);
DB::select("SELECT set_config('application_name',?,false)", [$p['application_name']]);
$this->withoutExceptionHandling();
$user = User::findOrFail($p['actor']);
$token = $p['context'] ?? bin2hex(random_bytes(24));
$this->actingAs($user)->withSession(['auth_version' => $user->auth_version, 'company_context' => [
'tenant_id' => $p['tenant'], 'company_id' => $p['company'], 'unit_id' => $p['unit'], 'module_id' => null, 'token' => $token,
]])->withHeader('X-SGF-Context', $token);
$result = ['mode' => $p['mode'], 'requests' => []];
if ($p['mode'] === 'read') {
DB::beginTransaction(); DB::statement('SET TRANSACTION READ ONLY'); DB::statement("SET LOCAL statement_timeout = '3s'");
try {
$this->getJson($p['url'])->assertOk();
$this->barrier($p);
$metrics = null;
DB::listen(function ($q) use (&$metrics) {
if ($metrics === null) return;
if (!preg_match('/^\s*(select|with)\b/i', $q->sql)) throw new \RuntimeException('READ_WORKER_WROTE_SQL');
$metrics['queries']++; $metrics['database_ms'] += $q->time;
});
for ($i = 0; $i < $p['requests']; $i++) {
gc_collect_cycles(); memory_reset_peak_usage(); $memory = memory_get_usage(false);
$metrics = ['queries' => 0, 'database_ms' => 0.0]; $start = hrtime(true); $wall = microtime(true);
try {
$r = $this->getJson($p['url']);
$elapsed = (hrtime(true) - $start) / 1e6; $finished = microtime(true); $stats = $metrics; $metrics = null;
$peak = memory_get_peak_usage(false);
$r->assertOk()->assertJsonPath('pagination.total', $p['total']);
$this->assertSame($p['response_sha256'], hash('sha256', $r->getContent()));
$result['requests'][] = ['status' => 'ok', 'elapsed_ms' => round($elapsed, 3), 'started_at' => $wall, 'finished_at' => $finished,
'queries' => $stats['queries'], 'database_ms' => round($stats['database_ms'], 3), 'incremental_php_bytes' => max(0, $peak - $memory), 'peak_php_bytes' => $peak];
} catch (\Illuminate\Database\QueryException $e) {
$metrics = null; $result['requests'][] = ['status' => 'sql_timeout_or_error', 'sqlstate' => $e->errorInfo[0] ?? $e->getCode(), 'elapsed_ms' => round((hrtime(true) - $start) / 1e6, 3)]; break;
}
}
} finally { DB::rollBack(); }
} elseif (in_array($p['mode'], ['confirm', 'confirm_http', 'preview_http'], true)) {
app(CompanyContext::class)->selectFor($p['actor'], $p['tenant'], $p['company'], $p['unit']);
$timeout = (int) ($p['lock_timeout_ms'] ?? 1000);
if ($timeout < 100 || $timeout > 10000) throw new \RuntimeException('Unexpected diagnostic lock timeout');
DB::select("SELECT set_config('lock_timeout',?,false)", [$timeout.'ms']);
DB::select("SELECT set_config('statement_timeout',?,false)", [isset($p['lock_timeout_ms']) ? '12s' : '5s']);
$this->barrier($p); $start = hrtime(true);
try {
if ($p['mode'] !== 'confirm') {
$this->withExceptionHandling();
$response = $this->postJson($p['mode'] === 'preview_http' ? '/api/module-records/preview' : '/api/module-records',
$p['mode'] === 'preview_http' ? $p['preview_input'] : ['review_token' => $p['review_token']]);
$response->assertStatus(423)->assertExactJson(['code' => 'association_busy',
'message' => 'A conferência está ocupada por outra operação. Aguarde e volte a confirmar; o estado será novamente validado.']);
$this->assertSame('no-store, private', $response->headers->get('Cache-Control'));
$result['confirmation'] = ['status' => 'http_busy', 'http_status' => $response->status(), 'code' => $response->json('code'),
'body_keys' => array_keys($response->json()), 'elapsed_ms' => round((hrtime(true) - $start) / 1e6, 3)];
} else {
$r = Request::create('/api/module-records', 'POST', ['review_token' => $p['review_token']]); $r->headers->set('X-SGF-Context', $token);
$result['confirmation'] = ['status' => ($p['expected_result'] ?? false) ? 'completed' : 'unexpected_success', 'result' => app(ModuleAssociationReview::class)->confirm($r), 'elapsed_ms' => round((hrtime(true) - $start) / 1e6, 3)];
}
} catch (\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e) {
$result['confirmation'] = ['status' => 'refused', 'http_status' => $e->getStatusCode(), 'elapsed_ms' => round((hrtime(true) - $start) / 1e6, 3)];
} catch (\Illuminate\Database\QueryException $e) {
$result['confirmation'] = ['status' => 'database_error', 'sqlstate' => $e->errorInfo[0] ?? $e->getCode(), 'elapsed_ms' => round((hrtime(true) - $start) / 1e6, 3)];
}
} else { throw new \RuntimeException('Unknown worker mode'); }
$result['finished_at'] = microtime(true);
file_put_contents($p['result'], json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
}
private function barrier(array $p): void
{
file_put_contents($p['ready'], 'ready'); $deadline = microtime(true) + 45;
while (!is_file($p['go'])) { if (microtime(true) > $deadline) throw new \RuntimeException('Worker barrier timed out'); usleep(5000); }
}
}