7.0.11 인스톨러·코어 업데이트 변경(e60a82d73)에 대해 과거 회귀 22건을 부류별로 대조한
결과, 신규 노출면 1건과 테스트 위생 1건이 나와 인터뷰 결정대로 조치했다.
1. argv 채널의 SAPI 게이트 — CGI/FPM 은 register_argc_argv=On 이면 $_SERVER['argv'] 를
쿼리스트링을 '+' 로 쪼개 채우므로(`GET /?x+core:update` → argv[1]==='core:update', php-cgi
실측) 비인증 웹 요청이 업데이트 트리로 판정되어 bootstrap/app.php 자가 치유가 요청마다
패키지 매니페스트를 지우고 다시 만들었다. CoreUpdateContext 와 bootstrap/app.php 복제본
모두 argv 를 cli·phpdbg 에서만 읽는다. env 플래그 채널은 웹에서 주입할 수 없으므로 그대로
두어 웹 요청 안에서 시작하는 업데이트 흐름(7.1.0)에 영향이 없다. 동형성 테스트에 SAPI 축을
더했다.
2. 매니페스트 삭제 실패 기록 — PackageManifestCacheHelper::clear 가 지우지 못한 파일의
경로를 돌려주고, spawn 직전 호출부가 업그레이드 로그·콘솔에 경고로 남긴다. 권한·소유권
불일치면 자식의 자가 치유도 같은 이유로 실패해 증상은 제보와 같은 「Class not found」 인데,
이 경고가 원인이 권한이라는 유일한 흔적이다.
3. 테스트 격리 — tests/bootstrap.php 가 APP_PACKAGES_CACHE/APP_SERVICES_CACHE 를 테스트
전용 경로로 돌린다. proc_open 으로 자식을 띄우는 기존 테스트 2종의 자식이 개발 클론의
실제 bootstrap/cache 매니페스트를 지우고 다시 쓰던 것(stat 실측)을 부모·자식 함께 막는다.
관리자 [시스템 최적화] 경로(withPreservedContainer 파사드 복원의 미실측 형제 호출처)는
임시 설치본에서 API 로 실측했다 — 200, 설정·라우트 캐시 재생성, 후속 요청 200, 로그 오류 0.
4. 트러블슈팅 사례 ↔ 회귀 테스트 앵커 계약 — 신규 사례는 헤딩에 <!-- case:{영역}-{번호} -->
앵커를 달고 같은 문자열을 그 사례를 잠그는 회귀 테스트에도 남겨야 한다. 사례 번호는
문서마다 1부터 재시작하고 병합으로 중복되므로(이번 리베이스에서도 우리 사례가 develop 과
같은 29 였다가 31 로 밀렸다), 개수만 대조하면 다른 사례를 덮는 테스트도 초록이 된다.
그런데 판정기 check-troubleshooting-test-coverage.cjs 를 부르는 지점이 저장소에 하나도
없었다 — 스크립트 자체 주석에만 실행법이 적혀 있어 아무도 부르지 않으면 영원히 돌지
않았고, 그 사이 위반이 9건 쌓였다(backend 26~31, cache 17~19). 돌지 않는 대조는 아무것도
잠그지 못하므로 위반 해소와 실행 지점 부여를 함께 한다.
9건 전부에 앵커를 부착하고(각 사례가 선언한 회귀 테스트 중 가장 구체적인 파일에 배치,
한 파일이 두 사례에 선언된 경우는 갈라 배치), stop-guard 7.2 에 앵커 계약 + 미커버
baseline ratchet 두 축으로 등록했다. 트러블슈팅 사례 추가 프로토콜에 6단계를
더하고 coverage 에 troubleshooting-case-anchor-contract(manual-only, 전용 판정기 위임)를
등재했다. 판정기 종료코드 1 → 0, 미커버 건수는 전 문서 baseline 그대로다.
174 lines
6.4 KiB
PHP
174 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Support;
|
|
|
|
use App\Support\CustomAssets;
|
|
use Illuminate\Support\Facades\File;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* [case:cache-19] `CustomAssets::publishableFiles()` — 게시·변경 감지가 공유하는 열거자 테스트 (#651 F7).
|
|
*
|
|
* 정적 게시는 `custom/**` 를 재귀로 복사하고, 변경 감지는 종전에 최상위 css/js 의 mtime 만
|
|
* 서명했다. 두 범위를 서로 다른 코드가 정의하면 어긋나고, 그 어긋남은 "글꼴을 바꿨는데
|
|
* 반영되지 않는다" 로만 나타난다. 이 열거자 하나가 두 소비자의 집합을 정한다.
|
|
*
|
|
* DB 를 쓰지 않는다 — 파일 시스템 해석만 검증한다.
|
|
*/
|
|
class CustomAssetsPublishableFilesTest extends TestCase
|
|
{
|
|
/** 테스트용 가짜 플러그인 식별자 */
|
|
private const FAKE_PLUGIN = 'g7test-publishable';
|
|
|
|
private function pluginDir(): string
|
|
{
|
|
return base_path('plugins/'.self::FAKE_PLUGIN);
|
|
}
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
CustomAssets::flushCache();
|
|
File::ensureDirectoryExists($this->pluginDir().'/custom');
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
File::deleteDirectory($this->pluginDir());
|
|
CustomAssets::flushCache();
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
private function writeCustom(string $relative, string $contents = '/* x */'): void
|
|
{
|
|
$path = $this->pluginDir().'/custom/'.$relative;
|
|
File::ensureDirectoryExists(dirname($path));
|
|
File::put($path, $contents);
|
|
}
|
|
|
|
/**
|
|
* 하위 디렉토리의 글꼴·이미지까지 열거한다 — 게시와 같은 범위.
|
|
*
|
|
* @scenario custom_source=convention_scan, custom_asset=static_file, custom_change_detection=nested_static_file
|
|
*
|
|
* @effects custom_signature_covers_publishable_tree
|
|
*/
|
|
#[Test]
|
|
public function 하위_디렉토리의_정적_파일까지_열거한다(): void
|
|
{
|
|
$this->writeCustom('custom.css');
|
|
$this->writeCustom('fonts/x.woff2', 'font');
|
|
$this->writeCustom('img/deep/marker.svg', '<svg/>');
|
|
|
|
$relatives = array_column(CustomAssets::publishableFiles('plugins', self::FAKE_PLUGIN), 'relative');
|
|
|
|
$this->assertSame(['custom.css', 'fonts/x.woff2', 'img/deep/marker.svg'], $relatives);
|
|
}
|
|
|
|
/**
|
|
* 허용 확장자 밖(텍스트·PHP)과 소스맵은 뺀다 — 게시 화이트리스트와 동일.
|
|
*
|
|
* @scenario custom_source=convention_scan, custom_asset=static_file, custom_change_detection=nested_static_file
|
|
*
|
|
* @effects custom_signature_covers_publishable_tree
|
|
*/
|
|
#[Test]
|
|
public function 허용_확장자_밖과_소스맵은_열거하지_않는다(): void
|
|
{
|
|
$this->writeCustom('custom.css');
|
|
$this->writeCustom('notes.txt', 'not an asset');
|
|
$this->writeCustom('evil.php', '<?php');
|
|
$this->writeCustom('custom.css.map', '{}');
|
|
|
|
$relatives = array_column(CustomAssets::publishableFiles('plugins', self::FAKE_PLUGIN), 'relative');
|
|
|
|
$this->assertSame(['custom.css'], $relatives);
|
|
}
|
|
|
|
/**
|
|
* 항목마다 절대 경로·수정 시각·크기를 싣는다 — 서명 재료이자 게시 복사 원본이다.
|
|
*
|
|
* @scenario custom_source=convention_scan, custom_asset=css, custom_change_detection=top_level_asset
|
|
*
|
|
* @effects custom_signature_covers_publishable_tree
|
|
*/
|
|
#[Test]
|
|
public function 항목은_절대경로_mtime_크기를_갖는다(): void
|
|
{
|
|
$this->writeCustom('custom.css', '/* twelve b */');
|
|
|
|
$files = CustomAssets::publishableFiles('plugins', self::FAKE_PLUGIN);
|
|
|
|
$this->assertCount(1, $files);
|
|
$this->assertSame('custom.css', $files[0]['relative']);
|
|
$this->assertFileExists($files[0]['absolute']);
|
|
$this->assertSame(14, $files[0]['size']);
|
|
$this->assertGreaterThan(0, $files[0]['mtime']);
|
|
}
|
|
|
|
/**
|
|
* 요청 스코프 메모이즈 — 같은 요청에서 두 번 물어도 디스크를 다시 훑지 않고, `flushCache()` 가 비운다.
|
|
*
|
|
* @scenario custom_source=convention_scan, custom_asset=css, custom_change_detection=no_change
|
|
*
|
|
* @effects custom_signature_covers_publishable_tree
|
|
*/
|
|
#[Test]
|
|
public function 결과는_요청_스코프로_메모이즈되고_flush_로_비워진다(): void
|
|
{
|
|
$this->writeCustom('custom.css');
|
|
|
|
$first = CustomAssets::publishableFiles('plugins', self::FAKE_PLUGIN);
|
|
|
|
// 메모가 살아 있는 동안 추가한 파일은 보이지 않는다
|
|
$this->writeCustom('fonts/x.woff2', 'font');
|
|
$this->assertSame($first, CustomAssets::publishableFiles('plugins', self::FAKE_PLUGIN));
|
|
|
|
CustomAssets::flushCache();
|
|
|
|
$this->assertCount(2, CustomAssets::publishableFiles('plugins', self::FAKE_PLUGIN));
|
|
}
|
|
|
|
/**
|
|
* 디렉토리가 없거나 타입이 유효하지 않으면 빈 목록이다.
|
|
*
|
|
* @scenario custom_source=none, custom_asset=css, custom_change_detection=no_change
|
|
*
|
|
* @effects custom_signature_covers_publishable_tree
|
|
*/
|
|
#[Test]
|
|
public function 디렉토리_부재와_무효_타입은_빈_목록이다(): void
|
|
{
|
|
File::deleteDirectory($this->pluginDir());
|
|
|
|
$this->assertSame([], CustomAssets::publishableFiles('plugins', self::FAKE_PLUGIN));
|
|
$this->assertSame([], CustomAssets::publishableFiles('unknown', self::FAKE_PLUGIN));
|
|
$this->assertSame([], CustomAssets::publishableFiles('plugins', '../escape'));
|
|
}
|
|
|
|
/**
|
|
* 로드 목록(`forExtension`)은 그대로 최상위 css/js 만 싣는다 — 열거자 도입이 로드 의미를 바꾸지 않는다.
|
|
*
|
|
* @scenario custom_source=convention_scan, custom_asset=static_file, custom_change_detection=nested_static_file
|
|
*
|
|
* @effects custom_signature_covers_publishable_tree
|
|
*/
|
|
#[Test]
|
|
public function 로드_목록은_여전히_최상위_css_js_만_싣는다(): void
|
|
{
|
|
$this->writeCustom('custom.css');
|
|
$this->writeCustom('fonts/inner.css');
|
|
$this->writeCustom('img/marker.svg', '<svg/>');
|
|
|
|
$loaded = CustomAssets::forExtension('plugins', self::FAKE_PLUGIN);
|
|
$publishable = CustomAssets::publishableFiles('plugins', self::FAKE_PLUGIN);
|
|
|
|
$this->assertCount(1, $loaded);
|
|
$this->assertStringContainsString('custom.css', $loaded[0]['url']);
|
|
$this->assertCount(3, $publishable);
|
|
}
|
|
}
|