Laravel PackageManifest 는 bootstrap/cache/packages.php 가 있으면 stale 여부를 검사하지 않고 그대로 읽어 등재된 provider 를 new 한다. 코어 업데이트는 Step 6/8 에서 vendor 를 --no-dev 로 교체하지만 그 파일은 Step 11 까지 이전 설치본의 것이 남으므로, 옵션 없는 `composer install` 로 깔린 사이트에서는 Step 10 spawn 자식이 새 vendor 에 없는 provider 를 찾다 부팅 단계에서 죽는다. 부팅 전이라 앱 로그에 흔적이 없고 부모에게는 자식의 비정상 종료로만 보여, 운영자에게는 「Class ... not found」 와 수동 재개 안내만 남는다. (sir.kr 커뮤니티 제보, 7.0.9 → 7.0.10) 3계층으로 막는다. 1. 부모 — spawn 직전 PackageManifestCacheHelper::clear 2. 자식 — bootstrap/app.php 가 G7_UPDATE_IN_PROGRESS 를 보고 스스로 정리한다. 이미 배포된 7.0.9·7.0.10 부모는 고칠 수 없으므로 그 아래에서 도는 신버전 자식의 유일한 방어다. App\ 클래스를 참조하지 않고 실패는 무시한다. 3. 범위 — CoreVersionChecker 의 env APP_VERSION 우선을 CoreUpdateContext 트리 안으로 축소한다. 업데이트 전에 뜬 artisan serve·큐 워커가 옛 값을 물고 확장을 incompatible_core 로 끄던 경로를 닫는다(관리자 템플릿이 대상이면 복구 UI 자체에 도달할 수 없다). 업데이트 트리 판정은 App\Support\CoreUpdateContext 가 단독 소유하고 CoreServiceProvider::isCoreUpdateInProgress 는 위임으로 남는다. bootstrap/app.php 의 복제본은 부팅 전이라 불가피한 예외이며, 두 조건의 동형성을 테스트가 단언한다. 실측 중 드러난 결함 2건을 함께 고쳤다. - ConfigCacheHelper::withPreservedContainer 가 파사드 애플리케이션을 되돌리지 않아 Step 11 이 `Target class [command.tinker] does not exist` 로 실패·롤백했다. - updateVersionInEnv 가 프로세스 환경을 갱신하지 않아 config 캐시에 이전 버전이 구워졌다 (Laravel env 저장소가 불변이라 재부팅으로도 덮이지 않는다). 인스톨러는 재사용 vendor 의 개발용 패키지를 installed.json 으로 감지해 설치 환경 확인 카드·설치 로그로 알리되 설치를 차단하지 않고( 결정 D1), 재사용 경로에서도 컴파일 캐시를 정리한다. 실행되는 명령만이 아니라 실패 시 안내하는 수동 명령까지 --no-dev 로 맞췄다. 코어 업데이트 완료·핸드오프·단독 재개 사후 단계에서 queue:restart 신호를 보낸다(D3). 코어 7.0.10 → 7.0.11.
158 lines
6.5 KiB
PHP
158 lines
6.5 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Support;
|
|
|
|
use App\Support\ConfigCacheHelper;
|
|
use Illuminate\Container\Container;
|
|
use Illuminate\Contracts\Console\Kernel;
|
|
use Illuminate\Support\Facades\Facade;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* `ConfigCacheHelper` 가 `config:cache` 뒤 전역 컨테이너 인스턴스를 되돌리는지 (#651 F2 실측 파생).
|
|
*
|
|
* `config:cache` 는 새 Application 을 부팅하고 그 생성자가 `Container::setInstance()` 를 호출한다.
|
|
* 되돌리지 않으면 같은 프로세스의 후속 `app()->terminating()` 이 종료되지 않는 일회용 앱에 등록되어
|
|
* 정적 재게시 예약이 조용히 사라진다 — 예외도 로그도 없이 "다음 렌더에야 반영" 으로만 나타난다.
|
|
*/
|
|
class ConfigCacheHelperContainerTest extends TestCase
|
|
{
|
|
/**
|
|
* 콜백이 전역 인스턴스를 바꿔 놓아도 실행 후 원래 앱으로 돌아온다.
|
|
*
|
|
* @effects config_cache_rebuild_preserves_container_instance
|
|
*/
|
|
#[Test]
|
|
public function 콜백이_전역_인스턴스를_바꿔도_원래_앱으로_되돌린다(): void
|
|
{
|
|
$original = Container::getInstance();
|
|
$stray = new Container;
|
|
|
|
ConfigCacheHelper::withPreservedContainer(static function () use ($stray): void {
|
|
Container::setInstance($stray);
|
|
});
|
|
|
|
$this->assertSame($original, Container::getInstance(), 'config:cache 뒤 app() 이 일회용 앱을 가리킨 채 남는다 — 후속 terminating 예약이 사라진다');
|
|
$this->assertSame($original, app());
|
|
}
|
|
|
|
/**
|
|
* 콜백이 예외를 던져도 인스턴스는 되돌린다.
|
|
*
|
|
* @effects config_cache_rebuild_preserves_container_instance
|
|
*/
|
|
#[Test]
|
|
public function 콜백이_실패해도_원래_앱으로_되돌린다(): void
|
|
{
|
|
$original = Container::getInstance();
|
|
|
|
try {
|
|
ConfigCacheHelper::withPreservedContainer(static function (): void {
|
|
Container::setInstance(new Container);
|
|
|
|
throw new \RuntimeException('boom');
|
|
});
|
|
$this->fail('예외가 전파되어야 한다');
|
|
} catch (\RuntimeException) {
|
|
// 기대한 예외
|
|
}
|
|
|
|
$this->assertSame($original, Container::getInstance());
|
|
}
|
|
|
|
/**
|
|
* 콜백이 파사드 애플리케이션을 바꿔 놓아도 실행 후 원래 앱으로 돌아온다.
|
|
*
|
|
* `config:cache` 가 부팅하는 일회용 앱의 `registerBaseBindings()` 는 컨테이너 인스턴스만이 아니라
|
|
* `Facade::clearResolvedInstances()` + `Facade::setFacadeApplication()` 도 호출한다. 컨테이너만
|
|
* 되돌리면 그 뒤의 **모든 파사드 호출**(`Artisan`·`Log`·`DB`·`Cache` …)이 종료된 일회용 앱에서
|
|
* 인스턴스를 해석한다.
|
|
*
|
|
* 코어 업데이트에서 드러난 형태: Step 11 의 `ConfigCacheHelper::rebuild()` 뒤에 오는
|
|
* `RouteCacheHelper::rebuild()` 의 `Artisan::call('route:clear')` 가 일회용 앱에서 **새 콘솔 Kernel**
|
|
* 을 해석하고, 그 Kernel 이 콘솔 Application 을 처음부터 구성하며 등록된 커맨드를 전부 resolve 해
|
|
* `Target class [command.tinker] does not exist.` 로 업데이트가 실패·롤백했다 (2026-09-07 실측).
|
|
*
|
|
* @effects config_cache_rebuild_preserves_facade_application
|
|
*/
|
|
#[Test]
|
|
public function 콜백이_파사드_애플리케이션을_바꿔도_원래_앱으로_되돌린다(): void
|
|
{
|
|
$original = Facade::getFacadeApplication();
|
|
$this->assertNotNull($original, '전제: 파사드 애플리케이션이 설정되어 있다');
|
|
|
|
ConfigCacheHelper::withPreservedContainer(static function (): void {
|
|
// 일회용 앱 부팅이 하는 일과 같은 형태 — 해석 캐시를 비우고 다른 앱을 가리킨다.
|
|
Facade::clearResolvedInstances();
|
|
Facade::setFacadeApplication(new Container);
|
|
});
|
|
|
|
$this->assertSame(
|
|
$original,
|
|
Facade::getFacadeApplication(),
|
|
'config:cache 뒤 파사드가 일회용 앱을 가리킨 채 남으면 후속 Artisan::call 이 새 콘솔 Kernel 을 만든다'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 콜백이 예외를 던져도 파사드 애플리케이션은 되돌린다.
|
|
*
|
|
* @effects config_cache_rebuild_preserves_facade_application
|
|
*/
|
|
#[Test]
|
|
public function 콜백이_실패해도_파사드_애플리케이션을_되돌린다(): void
|
|
{
|
|
$original = Facade::getFacadeApplication();
|
|
|
|
try {
|
|
ConfigCacheHelper::withPreservedContainer(static function (): void {
|
|
Facade::setFacadeApplication(new Container);
|
|
|
|
throw new \RuntimeException('boom');
|
|
});
|
|
$this->fail('예외가 전파되어야 한다');
|
|
} catch (\RuntimeException) {
|
|
// 기대한 예외
|
|
}
|
|
|
|
$this->assertSame($original, Facade::getFacadeApplication());
|
|
}
|
|
|
|
/**
|
|
* 되돌린 뒤 파사드가 원래 앱의 인스턴스를 해석한다 (해석 캐시 오염 없음).
|
|
*
|
|
* @effects config_cache_rebuild_preserves_facade_application
|
|
*/
|
|
#[Test]
|
|
public function 되돌린_뒤_파사드가_원래_앱의_인스턴스를_해석한다(): void
|
|
{
|
|
$original = Facade::getFacadeApplication();
|
|
$kernelBefore = app(Kernel::class);
|
|
|
|
ConfigCacheHelper::withPreservedContainer(static function (): void {
|
|
Facade::clearResolvedInstances();
|
|
Facade::setFacadeApplication(new Container);
|
|
});
|
|
|
|
$this->assertSame($original, Facade::getFacadeApplication());
|
|
$this->assertSame($kernelBefore, app(Kernel::class), '콘솔 Kernel 이 새로 만들어지면 등록된 커맨드를 전부 다시 resolve 한다');
|
|
}
|
|
|
|
/**
|
|
* `rebuild()` 가 `config:cache` 호출을 보존 래퍼로 감싼다 (소스 구조).
|
|
*
|
|
* @effects config_cache_rebuild_preserves_container_instance
|
|
*/
|
|
#[Test]
|
|
public function rebuild_는_config_cache_를_보존_래퍼로_감싼다(): void
|
|
{
|
|
$method = new \ReflectionMethod(ConfigCacheHelper::class, 'rebuild');
|
|
$lines = file($method->getFileName()) ?: [];
|
|
$body = implode('', array_slice($lines, $method->getStartLine() - 1, $method->getEndLine() - $method->getStartLine() + 1));
|
|
|
|
$this->assertStringContainsString("withPreservedContainer(static fn () => Artisan::call('config:cache'))", $body);
|
|
$this->assertStringNotContainsString("\n Artisan::call('config:cache');", $body);
|
|
}
|
|
}
|