tmpServicePath = base_path('app/Services/TestUpgradeSpawnMarkerService.php'); $this->tmpUpgradePath = base_path('upgrades/Upgrade_0_0_1_test_spawn.php'); $this->markerPath = storage_path('app/test-upgrade-spawn-marker-'.uniqid().'.txt'); } protected function tearDown(): void { foreach ([$this->tmpServicePath, $this->tmpUpgradePath, $this->markerPath] as $p) { if (File::exists($p)) { File::delete($p); } } parent::tearDown(); } public function test_spawned_process_loads_fresh_classes(): void { if (! function_exists('proc_open')) { $this->markTestSkipped('proc_open 미지원 환경'); } $markerPath = addslashes($this->markerPath); $serviceCode = <<tmpServicePath, $serviceCode); File::put($this->tmpUpgradePath, $upgradeCode); $this->assertFileDoesNotExist($this->markerPath, '사전: 마커 파일이 없어야 한다'); $phpBinary = PHP_BINARY; $artisan = base_path('artisan'); $commandLine = implode(' ', array_map('escapeshellarg', [ $phpBinary, $artisan, 'core:execute-upgrade-steps', '--from=0.0.0', '--to=0.0.1', '--force', '--env=testing', // 본 테스트가 보는 것은 "spawn 된 자식이 방금 쓴 클래스를 로드하는가" 뿐이다. // 이 옵션이 없으면 자식이 `.env` 의 APP_VERSION 을 0.0.1 로 덮어쓴다 — // `--env=testing` 은 읽는 파일만 바꿀 뿐 쓰기 대상은 언제나 `base_path('.env')` // 이므로 개발자의 실제 `.env` 가 훼손된다. 자식 프로세스라 부모의 base path // 격리도, 테스트 부트스트랩의 복원 안전망도 닿지 않는다. '--skip-version-env', ])).' 2>&1'; $descriptors = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; $process = proc_open($commandLine, $descriptors, $pipes, base_path()); $this->assertIsResource($process, 'proc_open 으로 별도 프로세스 실행 성공해야 한다'); fclose($pipes[0]); $stdout = stream_get_contents($pipes[1]); fclose($pipes[1]); fclose($pipes[2]); $exitCode = proc_close($process); $this->assertSame( 0, $exitCode, "spawn 프로세스가 exit 0 으로 종료되어야 한다. output:\n{$stdout}" ); $this->assertFileExists( $this->markerPath, '별도 프로세스가 최신 Service 를 autoload 로 로드하여 마커를 작성해야 한다' ); $marker = File::get($this->markerPath); $this->assertStringStartsWith('spawned:', $marker, '마커 내용이 예상 형식이어야 한다'); } }