['index' => $source['index'], 'status' => 'processed', 'lines' => [['text' => 'SYNTHETIC_TEXT', 'confidence' => 0.9, 'box' => [[0, 0], [1, 0], [1, 1], [0, 1]]]]], $request['images']); $result = ['status' => 'completed', 'images' => $rows]; if ($scenario === 'missing') array_pop($result['images']); if ($scenario === 'duplicate') $result['images'][1]['index'] = 7; if ($scenario === 'unknown') $result['images'][1]['index'] = 999; if ($scenario === 'confidence') $result['images'][0]['lines'][0]['confidence'] = 2; if ($scenario === 'box') $result['images'][0]['lines'][0]['box'][0][0] = 'bad'; if ($scenario === 'control') $result['images'][0]['lines'][0]['text'] = "bad\0text"; if ($scenario === 'no-lines') $result['images'][0]['lines'] = []; if ($scenario === 'unavailable') $result['status'] = 'unavailable'; if ($scenario === 'partial') $result['images'][1] = ['index' => 19, 'status' => 'unreadable', 'lines' => []]; fwrite($descriptors[1], $scenario === 'malformed' ? '{' : json_encode($result)); if ($scenario === 'overflow') fwrite($descriptors[1], str_repeat('x', 524289)); fwrite($descriptors[2], 'SYNTHETIC_PRIVATE_STDERR'); $GLOBALS['ocrRunning'] = in_array($scenario, ['timeout', 'cancel'], true); return fopen('php://temp', 'w+b'); } function proc_get_status($process): array { return ['running' => $GLOBALS['ocrRunning'], 'exitcode' => $GLOBALS['ocrScenario'] === 'exit' ? 1 : 0]; } function proc_terminate($process, $signal = 15): bool { $GLOBALS['ocrRunning'] = false; $GLOBALS['ocrTerminated']++; return true; } function proc_close($process): int { if ($GLOBALS['ocrRunning']) throw new \RuntimeException('must never close a running process'); $GLOBALS['ocrClosed']++; fclose($process); return 0; } } namespace { require dirname(__DIR__) . '/app/common/service/ClinicalImageTextService.php'; use app\common\service\ClinicalImageTextService; function ocrExpect(bool $condition, string $message): void { if (!$condition) throw new RuntimeException($message); } $directory = sys_get_temp_dir() . '/clinical-ocr-fixture-' . bin2hex(random_bytes(8)); mkdir($directory, 0700); $image = $directory . '/image.png'; file_put_contents($image, base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=')); $images = [['index' => 7, 'path' => $image], ['index' => 19, 'path' => $image]]; $config = ['enabled' => true, 'python' => PHP_BINARY, 'timeout_seconds' => 1, 'threads' => 1, 'detection_limit' => 640, 'max_text_bytes' => 4096]; $ocrCommands = []; $ocrTemporaryPaths = []; $ocrClosed = 0; $ocrTerminated = 0; $ocrRunning = false; putenv('SYNTHETIC_PRIVATE_SECRET=not-for-child'); try { $disabled = ClinicalImageTextService::read($images, $directory, microtime(true) + 1, null, ['enabled' => false]); ocrExpect($disabled === ['status' => 'disabled', 'images' => []] && $ocrCommands === [], 'disabled feature performs no subprocess work'); foreach (['success', 'partial', 'missing', 'duplicate', 'unknown', 'confidence', 'box', 'control', 'no-lines', 'malformed', 'overflow', 'exit', 'spawn', 'timeout', 'cancel', 'unavailable'] as $ocrScenario) { $ocrRunning = false; $before = count($ocrCommands); $checks = 0; $abort = $ocrScenario === 'cancel' ? static function () use (&$checks): bool { return ++$checks > 1; } : null; $result = ClinicalImageTextService::read($images, $directory, microtime(true) + ($ocrScenario === 'timeout' ? 0.06 : 1), $abort, $config); ocrExpect(count($ocrCommands) === $before + 1, 'one subprocess per complete source set'); ocrExpect(count($result['images']) === 2 && array_column($result['images'], 'index') === [7, 19], 'every input index explicitly covered on every outcome'); if ($ocrScenario === 'success') ocrExpect($result['status'] === 'completed', 'valid complete output'); elseif (in_array($ocrScenario, ['partial', 'missing', 'unavailable', 'timeout', 'cancel'], true)) ocrExpect($result['status'] === 'partial', 'checkpoint/missing rows never claim a fully completed set'); else ocrExpect($result['status'] === 'unavailable', 'invalid child output is safe optional unavailability'); ocrExpect(!str_contains(json_encode($result), 'SYNTHETIC_PRIVATE_STDERR'), 'stderr never reaches callers'); foreach ($ocrTemporaryPaths as $path) ocrExpect(!file_exists($path), 'private request descriptors are cleaned after every outcome'); } foreach (['duplicate', 'outside', 'url', 'unc', 'relative', 'index', 'pixel', 'bytes'] as $invalid) { $selected = $images; if ($invalid === 'duplicate') $selected[1]['index'] = 7; elseif ($invalid === 'index') $selected[0]['index'] = true; elseif ($invalid === 'outside') $selected[0]['path'] = __FILE__; elseif ($invalid === 'url') $selected[0]['path'] = 'https://synthetic.invalid/image.png'; elseif ($invalid === 'unc') $selected[0]['path'] = '\\\\synthetic.invalid\\share\\image.png'; elseif ($invalid === 'relative') $selected[0]['path'] = 'image.png'; elseif ($invalid === 'bytes') { $huge = $directory . '/large.png'; $handle = fopen($huge, 'w+b'); ftruncate($handle, 20971521); fclose($handle); $selected[0]['path'] = $huge; } else { $huge = $directory . '/pixels.bmp'; file_put_contents($huge, 'BM' . pack('VvvV', 54, 0, 0, 54) . pack('VllvvVVllVV', 40, 10000, 10000, 1, 24, 0, 0, 0, 0, 0, 0)); $selected[0]['path'] = $huge; } $before = count($ocrCommands); $result = ClinicalImageTextService::read($selected, $directory, microtime(true) + 1, null, $config); ocrExpect($result['status'] === 'unavailable' && count($ocrCommands) === $before, 'invalid source rejected before process creation: ' . $invalid); } ocrExpect($ocrTerminated >= 2 && $ocrClosed >= 2, 'deadline/cancel terminate and reap owned subprocesses'); echo "ClinicalImageTextService offline descriptors/coverage/limits/cancellation: 25 scenarios OK\n"; } finally { putenv('SYNTHETIC_PRIVATE_SECRET'); foreach (glob($directory . '/*') ?: [] as $path) unlink($path); rmdir($directory); } }