upgradesPath = base_path('upgrades'); $this->handoffStepPath = $this->upgradesPath.'/Upgrade_0_0_1_test_handoff.php'; $this->failingStepPath = $this->upgradesPath.'/Upgrade_0_0_1_test_failing.php'; } protected function tearDown(): void { foreach ([$this->handoffStepPath, $this->failingStepPath] as $path) { if (File::exists($path)) { File::delete($path); } } parent::tearDown(); } /** * UpgradeHandoffException 은 runUpgradeSteps 를 뚫고 상위로 그대로 전파되어야 한다. */ public function test_handoff_exception_propagates_up(): void { $code = <<<'PHP' handoffStepPath, $code); $service = new CoreUpdateService; try { $service->runUpgradeSteps('0.0.0', '0.0.1', null, true); $this->fail('UpgradeHandoffException 이 상위로 전파되어야 한다'); } catch (UpgradeHandoffException $e) { $this->assertSame('0.0.0', $e->afterVersion); $this->assertSame('테스트 핸드오프', $e->reason); } } /** * 일반 RuntimeException 도 상위로 그대로 전파되어야 한다 (catch 안 함). */ public function test_generic_exception_propagates_up(): void { $code = <<<'PHP' failingStepPath, $code); $service = new CoreUpdateService; try { $service->runUpgradeSteps('0.0.0', '0.0.1', null, true); $this->fail('RuntimeException 이 상위로 전파되어야 한다'); } catch (RuntimeException $e) { // UpgradeHandoffException 도 RuntimeException 상속이므로 구체 타입 확인 $this->assertNotInstanceOf(UpgradeHandoffException::class, $e); $this->assertSame('테스트 일반 실패', $e->getMessage()); } } /** * upgrades 디렉토리에 매칭 스텝이 없으면 조용히 반환 (예외 없음). * 경계 조건 검증. */ public function test_no_matching_steps_returns_silently(): void { $service = new CoreUpdateService; // 매우 작은 from~to 범위 — 어떤 실제 Upgrade_*.php 도 매칭되지 않음 $service->runUpgradeSteps('0.0.0', '0.0.0', null, false); $this->assertTrue(true, '예외 없이 종료되어야 한다'); } }