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.
182 lines
6.0 KiB
PHP
182 lines
6.0 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Support;
|
|
|
|
use App\Support\ComposerInstallInfo;
|
|
use Illuminate\Support\Facades\File;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* vendor 개발용 설치 판정 SSoT 테스트 (dev-g7 #658).
|
|
*
|
|
* 인스톨러(순수 PHP)와 코어 업데이트가 같은 vendor 를 두고 서로 다른 답을 내놓지 않도록
|
|
* 판정을 이 클래스 하나가 소유한다. "판정 불가"(null)와 "개발용 아님"(false)을 구분하는 것이
|
|
* 핵심이다 — 뭉뚱그리면 installed.json 이 없는 환경이 "운영용 구성 확인됨" 으로 보인다.
|
|
*/
|
|
class ComposerInstallInfoTest extends TestCase
|
|
{
|
|
private string $vendorPath;
|
|
|
|
private string $basePath;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->basePath = storage_path('framework/testing/composer-install-info-'.uniqid());
|
|
$this->vendorPath = $this->basePath.'/vendor';
|
|
|
|
File::ensureDirectoryExists($this->vendorPath.'/composer');
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
if (File::isDirectory($this->basePath)) {
|
|
File::deleteDirectory($this->basePath);
|
|
}
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* `dev: true` + 이름 목록 → 개발용 설치.
|
|
*
|
|
* @effects core_detects_dev_vendor_from_installed_json
|
|
*/
|
|
#[Test]
|
|
public function dev_true_와_이름_목록이_있으면_개발용_설치로_판정한다(): void
|
|
{
|
|
$this->writeInstalledJson([
|
|
'packages' => [],
|
|
'dev' => true,
|
|
'dev-package-names' => ['laravel/boost', 'laravel/mcp', 'laravel/pail'],
|
|
]);
|
|
|
|
$info = ComposerInstallInfo::inspect($this->vendorPath);
|
|
|
|
$this->assertTrue($info['dev']);
|
|
$this->assertSame(['laravel/boost', 'laravel/mcp', 'laravel/pail'], $info['packages']);
|
|
$this->assertTrue(ComposerInstallInfo::isDevInstall($this->vendorPath));
|
|
$this->assertCount(3, ComposerInstallInfo::devPackageNames($this->vendorPath));
|
|
}
|
|
|
|
/**
|
|
* `--no-dev` 설치 → 개발용 아님.
|
|
*
|
|
* @effects core_reports_no_dev_packages_for_no_dev_vendor
|
|
*/
|
|
#[Test]
|
|
public function dev_false_이고_이름_목록이_비면_운영용_설치로_판정한다(): void
|
|
{
|
|
$this->writeInstalledJson([
|
|
'packages' => [],
|
|
'dev' => false,
|
|
'dev-package-names' => [],
|
|
]);
|
|
|
|
$info = ComposerInstallInfo::inspect($this->vendorPath);
|
|
|
|
$this->assertFalse($info['dev']);
|
|
$this->assertSame([], $info['packages']);
|
|
$this->assertFalse(ComposerInstallInfo::isDevInstall($this->vendorPath));
|
|
$this->assertSame([], ComposerInstallInfo::devPackageNames($this->vendorPath));
|
|
}
|
|
|
|
/**
|
|
* `dev` 는 false 인데 이름 목록이 남아 있으면 개발용으로 본다 — 두 신호는 OR 다.
|
|
* Composer 버전에 따라 한쪽만 채워질 수 있고, 등재된 provider 가 있다는 사실이 중요하다.
|
|
*/
|
|
#[Test]
|
|
public function dev_false_라도_이름_목록이_비어있지_않으면_개발용으로_판정한다(): void
|
|
{
|
|
$this->writeInstalledJson([
|
|
'packages' => [],
|
|
'dev' => false,
|
|
'dev-package-names' => ['laravel/mcp'],
|
|
]);
|
|
|
|
$this->assertTrue(ComposerInstallInfo::isDevInstall($this->vendorPath));
|
|
}
|
|
|
|
/**
|
|
* installed.json 이 없으면 판정 불가(null) — "개발용 아님"(false)과 구분한다.
|
|
*
|
|
* @effects core_returns_unknown_when_installed_json_missing
|
|
*/
|
|
#[Test]
|
|
public function installed_json_이_없으면_판정_불가다(): void
|
|
{
|
|
$info = ComposerInstallInfo::inspect($this->vendorPath);
|
|
|
|
$this->assertNull($info['dev']);
|
|
$this->assertSame([], $info['packages']);
|
|
$this->assertNull(ComposerInstallInfo::isDevInstall($this->vendorPath));
|
|
}
|
|
|
|
/**
|
|
* JSON 파싱 실패도 판정 불가다.
|
|
*/
|
|
#[Test]
|
|
public function json_이_깨졌으면_판정_불가다(): void
|
|
{
|
|
File::put($this->vendorPath.'/'.ComposerInstallInfo::INSTALLED_JSON, '{ not json');
|
|
|
|
$this->assertNull(ComposerInstallInfo::isDevInstall($this->vendorPath));
|
|
}
|
|
|
|
/**
|
|
* Composer 1 형식(최상위가 패키지 배열)은 dev 정보가 없어 판정 불가다.
|
|
*/
|
|
#[Test]
|
|
public function composer1_형식은_판정_불가다(): void
|
|
{
|
|
File::put($this->vendorPath.'/'.ComposerInstallInfo::INSTALLED_JSON, json_encode([
|
|
['name' => 'vendor/pkg', 'version' => '1.0.0'],
|
|
]));
|
|
|
|
$this->assertNull(ComposerInstallInfo::isDevInstall($this->vendorPath));
|
|
}
|
|
|
|
/**
|
|
* 이름 목록의 비문자열·공백 항목은 걸러낸다 (건수를 그대로 화면에 표시하므로).
|
|
*/
|
|
#[Test]
|
|
public function 이름_목록의_비문자열과_공백_항목을_걸러낸다(): void
|
|
{
|
|
$this->writeInstalledJson([
|
|
'packages' => [],
|
|
'dev' => true,
|
|
'dev-package-names' => ['laravel/mcp', '', ' ', 123, null, ['nested'], 'laravel/pail'],
|
|
]);
|
|
|
|
$this->assertSame(['laravel/mcp', 'laravel/pail'], ComposerInstallInfo::devPackageNames($this->vendorPath));
|
|
}
|
|
|
|
/**
|
|
* 경로 끝의 구분자 유무와 무관하게 같은 결과를 낸다.
|
|
*/
|
|
#[Test]
|
|
public function 경로_끝_구분자_유무와_무관하게_동작한다(): void
|
|
{
|
|
$this->writeInstalledJson([
|
|
'packages' => [],
|
|
'dev' => true,
|
|
'dev-package-names' => ['laravel/mcp'],
|
|
]);
|
|
|
|
$this->assertTrue(ComposerInstallInfo::isDevInstall($this->vendorPath.'/'));
|
|
$this->assertTrue(ComposerInstallInfo::isDevInstall($this->vendorPath));
|
|
}
|
|
|
|
/**
|
|
* installed.json 픽스처를 씁니다.
|
|
*
|
|
* @param array<string, mixed> $payload JSON 으로 직렬화할 내용
|
|
*/
|
|
private function writeInstalledJson(array $payload): void
|
|
{
|
|
File::put($this->vendorPath.'/'.ComposerInstallInfo::INSTALLED_JSON, json_encode($payload));
|
|
}
|
|
}
|