This commit is contained in:
2026-11-02 20:27:11 +02:00
parent b44378b899
commit badaada043
300 changed files with 17458 additions and 1056 deletions
+151 -4
View File
@@ -18,21 +18,76 @@ config(['database.connections.context_proof_admin' => [...$config, 'database' =>
$admin = DB::connection('context_proof_admin');
$admin->statement('CREATE DATABASE "'.$database.'"');
$workflow = in_array('--workflow', $argv ?? [], true);
$warehouse = in_array('--warehouse', $argv ?? [], true);
$pilot = in_array('--pilot', $argv ?? [], true);
$workdesk = in_array('--workdesk', $argv ?? [], true);
$readiness = in_array('--readiness', $argv ?? [], true);
$exchange = in_array('--exchange', $argv ?? [], true);
$transport = in_array('--transport', $argv ?? [], true);
$internalTrace = in_array('--internal-trace', $argv ?? [], true);
$taskInbox = in_array('--task-inbox', $argv ?? [], true);
$financeIntegration = in_array('--finance-integration', $argv ?? [], true);
$awardTasks = in_array('--award-tasks', $argv ?? [], true);
$quotationTasks = in_array('--quotation-tasks', $argv ?? [], true);
$organizationPreview = in_array('--organization-preview', $argv ?? [], true);
$pilotAccessProposal = in_array('--pilot-access-proposal', $argv ?? [], true);
$formalTasks = in_array('--formal-tasks', $argv ?? [], true);
$inbox = in_array('--inbox', $argv ?? [], true) || $transport;
$warehouse = in_array('--warehouse', $argv ?? [], true) || $pilot || $workdesk || $readiness;
try {
$environment = ['APP_ENV' => 'testing', 'DB_CONNECTION' => 'pgsql', 'DB_URL' => '', 'DB_HOST' => $config['host'], 'DB_PORT' => (string) $config['port'],
'DB_DATABASE' => $database, 'DB_USERNAME' => $config['username'], 'DB_PASSWORD' => $config['password']];
$tests = $workflow ? ['tests/Feature/InternalRequisitionCandidateTest.php', 'tests/Feature/WorkContextTest.php'] : ['tests/Feature/WorkContextTest.php'];
if ($warehouse) {
$tests = ['tests/Feature/WarehouseRequestsTest.php', 'tests/Feature/WarehouseAvailabilityTest.php', 'tests/Feature/WarehouseFulfilmentTest.php'];
$tests = ['tests/Feature/WarehouseRequestsTest.php', 'tests/Feature/WarehouseAvailabilityTest.php', 'tests/Feature/WarehouseFulfilmentTest.php', 'tests/Feature/AutomaticPurchasesTest.php'];
}
$process = new Process([PHP_BINARY, 'vendor/bin/pest', '--compact', ...$tests], dirname(__DIR__), $environment, timeout: 120);
if ($readiness) {
$tests = [...$tests, 'tests/Feature/WarehousePilotReadinessTest.php', 'tests/Feature/WarehousePilotJourneyTest.php'];
}
if ($exchange) {
$tests = ['tests/Feature/WarehouseExchangeTest.php'];
}
if ($inbox) {
$environment['INBOX_TEST_PG'] = '1';
$tests = ['tests/Unit/WarehouseInboxTest.php', 'tests/Feature/WarehouseExchangeTest.php'];
}
if ($transport) {
$tests[] = 'tests/Unit/WarehouseHttpSourceTest.php';
}
if ($internalTrace) {
$tests = ['tests/Feature/InternalSupplyTraceTest.php', 'tests/Feature/WarehousePilotJourneyTest.php'];
}
if ($taskInbox) {
$tests = ['tests/Feature/WorkflowTaskInboxTest.php', 'tests/Feature/InternalSupplyTraceTest.php', 'tests/Feature/WarehousePilotJourneyTest.php'];
}
if ($financeIntegration) {
$tests = ['tests/Feature/FinanceIntegrationTest.php', 'tests/Feature/PaymentIntegrationTest.php', 'tests/Feature/InvoiceTest.php', 'tests/Feature/ExternalPaymentTest.php', 'tests/Feature/PurchaseProcessTest.php', 'tests/Feature/PayablePaymentDecisionTest.php'];
}
if ($awardTasks) {
$tests = ['tests/Feature/PurchaseAwardOrderTest.php'];
}
if ($quotationTasks) {
$tests = ['tests/Feature/SupplierQuotationTest.php', 'tests/Feature/PurchaseAwardOrderTest.php'];
}
if ($organizationPreview) {
$tests = ['tests/Feature/OrganizationActivationPreviewTest.php', 'tests/Feature/WorkContextTest.php'];
}
if ($pilotAccessProposal) {
$tests = ['tests/Feature/PilotAccessProposalTest.php'];
}
if ($formalTasks) {
$tests = ['tests/Feature/FormalAdmissionTest.php', 'tests/Feature/FormalRecommendationTest.php', 'tests/Feature/DocumentaryQualificationTest.php'];
}
$process = new Process([PHP_BINARY, 'vendor/bin/pest', '--compact', ...$tests, ...($formalTasks ? ['--filter=formal task'] : ($quotationTasks ? ['--filter=quotation tasks'] : ($awardTasks ? ['--filter=task inbox'] : [])))], dirname(__DIR__), $environment, timeout: $financeIntegration || $awardTasks || $quotationTasks || $organizationPreview || $pilotAccessProposal || $formalTasks ? 300 : 120);
$process->run();
echo $process->getOutput();
if (! $process->isSuccessful()) {
throw new RuntimeException('Prova PostgreSQL de contexto não aprovada.');
}
$races = [];
if ($inbox) {
require __DIR__.'/../tests/Support/inbox-pg-races.php';
$races = inboxPgRaces($environment, $config, $database);
}
if ($warehouse) {
config(['database.connections.warehouse_proof_read' => [...$config, 'database' => $database, 'url' => null]]);
$reader = DB::connection('warehouse_proof_read');
@@ -75,9 +130,101 @@ try {
}
$races[] = ['scenario' => $scenario, 'real_lock_contention' => true, 'result' => 'passed'];
}
$fixture = new Process([PHP_BINARY, 'tests/Concurrency/replenishment-worker.php', 'prepare'], dirname(__DIR__), $environment, timeout: 30);
$fixture->mustRun();
$data = json_decode($fixture->getOutput(), true, 32, JSON_THROW_ON_ERROR);
$first = new Process([PHP_BINARY, 'tests/Concurrency/replenishment-worker.php', 'hold', json_encode($data, JSON_THROW_ON_ERROR)], dirname(__DIR__), $environment, timeout: 30);
$first->start();
$deadline = microtime(true) + 5;
while ($first->isRunning() && ! str_contains($first->getOutput(), 'locked') && microtime(true) < $deadline) {
usleep(20000);
}
if (! str_contains($first->getOutput(), 'locked')) {
throw new RuntimeException('Falta bloqueio da compra automática.');
}
$second = new Process([PHP_BINARY, 'tests/Concurrency/replenishment-worker.php', 'run', json_encode($data, JSON_THROW_ON_ERROR)], dirname(__DIR__), $environment, timeout: 30);
$second->start();
$blocked = false;
$deadline = microtime(true) + 5;
while ($first->isRunning() && microtime(true) < $deadline) {
$blocked = (bool) $reader->selectOne("select exists(select 1 from pg_stat_activity where datname=? and wait_event_type='Lock') as blocked", [$database])->blocked;
if ($blocked) {
break;
}usleep(20000);
}
$first->wait();
$second->wait();
if (! $first->isSuccessful() || ! $second->isSuccessful()) {
throw new RuntimeException('Falha na activação concorrente: '.$first->getErrorOutput().$second->getErrorOutput());
}
$a = json_decode(trim(str_replace('locked', '', $first->getOutput())), true, 32, JSON_THROW_ON_ERROR);
$b = json_decode(trim($second->getOutput()), true, 32, JSON_THROW_ON_ERROR);
if (! $blocked || $a['rule'] !== $b['rule'] || count($a['purchases']) !== 1 || $a['purchases'] !== $b['purchases'] || (int) $reader->table('requisitions')->where('tenant_id', $data['tenant'])->where('status', 'draft')->count() !== 1) {
throw new RuntimeException('A reposição concorrente duplicou o rascunho.');
}
$races[] = ['scenario' => 'automatic_purchase_activation_retry', 'real_lock_contention' => true, 'single_draft' => true, 'result' => 'passed'];
if ($pilot) {
$fixture = new Process([PHP_BINARY, 'tests/Concurrency/recovery-worker.php', 'prepare'], dirname(__DIR__), $environment, timeout: 30);
$fixture->mustRun();
$data = json_decode($fixture->getOutput(), true, 32, JSON_THROW_ON_ERROR);
$before = $reader->table('replenishment_work')->where('id', $data['work'])->sole();
if ($before->generation <= $before->processed_generation || $reader->table('requisitions')->where('tenant_id', $data['tenant'])->where('status', 'draft')->exists()) {
throw new RuntimeException('A prova não deixou sinal durável sem rascunho após interrupção.');
}
$first = new Process([PHP_BINARY, 'tests/Concurrency/recovery-worker.php', 'hold', json_encode($data, JSON_THROW_ON_ERROR)], dirname(__DIR__), $environment, timeout: 30);
$first->start();
$deadline = microtime(true) + 5;
while ($first->isRunning() && ! str_contains($first->getOutput(), 'locked') && microtime(true) < $deadline) {
usleep(20000);
}
if (! str_contains($first->getOutput(), 'locked')) {
throw new RuntimeException('Primeiro recuperador sem bloqueio.');
}
$second = new Process([PHP_BINARY, 'tests/Concurrency/recovery-worker.php', 'run', json_encode($data, JSON_THROW_ON_ERROR)], dirname(__DIR__), $environment, timeout: 30);
$second->start();
$blocked = false;
$deadline = microtime(true) + 5;
while ($first->isRunning() && microtime(true) < $deadline) {
$blocked = (bool) $reader->selectOne("select exists(select 1 from pg_stat_activity where datname=? and wait_event_type='Lock') as blocked", [$database])->blocked;
if ($blocked) {
break;
}usleep(20000);
}
$first->wait();
$second->wait();
if (! $first->isSuccessful() || ! $second->isSuccessful()) {
throw new RuntimeException('Recuperação concorrente falhou.');
}
$a = json_decode(trim(str_replace('locked', '', $first->getOutput())), true, 32, JSON_THROW_ON_ERROR);
$b = json_decode(trim($second->getOutput()), true, 32, JSON_THROW_ON_ERROR);
$after = $reader->table('replenishment_work')->where('id', $data['work'])->sole();
if (! $blocked || $a['result'] !== 'saved' || $b['result'] !== 'saved' || $a['authenticated'] || $b['authenticated'] || count($a['drafts']) !== 1 || $a['drafts'] !== $b['drafts'] || $after->generation !== $after->processed_generation) {
throw new RuntimeException('A recuperação perdeu ou duplicou a compra.');
}
$races[] = ['scenario' => 'abrupt_exit_after_physical_commit_then_two_recovery_workers', 'real_lock_contention' => true, 'single_draft' => true, 'no_authenticated_human' => true, 'result' => 'passed'];
}
DB::purge('warehouse_proof_read');
}
file_put_contents(dirname(__DIR__).'/docs/proofs/'.($warehouse ? 'warehouse-drafts-pg' : ($workflow ? 'workflow-notices-pg' : 'work-context-pg')).'.json', json_encode(['checked_at' => gmdate('c'), 'database' => 'disposable PostgreSQL', 'result' => trim($process->getOutput()), 'races' => $races, 'persistent_data_changed' => false, 'database_removed_in_finally' => true], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
$proofName = match (true) {
$formalTasks => 'formal-task-integration-pg',
$pilotAccessProposal => 'pilot-access-proposal-pg',
$organizationPreview => 'organization-activation-preview-pg',
$quotationTasks => 'quotation-task-integration-pg',
$awardTasks => 'award-task-integration-pg',
$financeIntegration => 'finance-integration-pg',
$taskInbox => 'workflow-task-inbox-pg',
$internalTrace => 'internal-supply-integration-pg',
$transport => 'warehouse-http-source-pg',
$inbox => 'warehouse-inbox-pg',
$exchange => 'warehouse-exchange-pg',
$readiness => 'warehouse-pilot-readiness-pg',
$workdesk => 'warehouse-workdesk-pg',
$pilot => 'replenishment-pilot-pg',
$warehouse => 'warehouse-drafts-pg',
$workflow => 'workflow-notices-pg',
default => 'work-context-pg',
};
file_put_contents(dirname(__DIR__).'/docs/proofs/'.$proofName.'.json', json_encode(['checked_at' => gmdate('c'), 'database' => 'disposable PostgreSQL', 'result' => trim($process->getOutput()), 'races' => $races, 'persistent_data_changed' => false, 'database_removed_in_finally' => true], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
} finally {
$admin->select('select pg_terminate_backend(pid) from pg_stat_activity where datname = ? and pid <> pg_backend_pid()', [$database]);
$admin->statement('DROP DATABASE "'.$database.'"');