core:update Step 7 이 targets 전체를 무조건 재복사하고 orphan 을 삭제하여 사용자 .htaccess 커스텀 블록·_bundled 커스텀 확장이 소실되던 사고(공개 )를 차단한다. 백업(base)↔_pending(theirs) 3-way md5 비교로 코어가 실제 변경/추가한 파일만 산출해 적용하고, 코어 미변경 파일은 복사·권한·mtime 을 전부 스킵해 현재 디스크 상태(사용자 수정 포함)를 보존한다. 전체 덮어쓰기 + orphan 정리가 필요하면 --prune 으로 opt-in. 백업 부재(--no-backup) 시 base 가 없어 전체 덮어쓰기로 안전 회귀 + 안내를 출력한다. 기존 stale 정리 테스트는 새 기본(증분)에서 orphan 을 삭제하지 않으므로 prune 컨텍스트로 전환했고, 증분 3-way 판정·목록 적용·fallback 단위 테스트와 --prune 커맨드 표면 테스트를 추가했다. 번들 ja 언어팩은 코어 apply_mode 안내 키(g7-core-ja)와 함께, 같은 빌드에서 동기화된 관리자↔유저 크로스링크 라벨(board/ecommerce/admin_basic/basic)의 일본어 번역을 반영하고 각 패키지 버전·CHANGELOG 를 갱신했다.
255 lines
8.8 KiB
PHP
255 lines
8.8 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Extension;
|
|
|
|
use App\Extension\Helpers\CoreBackupHelper;
|
|
use Illuminate\Support\Facades\File;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* CoreBackupHelper::computeApplyList() 단위 테스트 (공개 #64)
|
|
*
|
|
* `core:update` 기본(증분) 모드의 3-way 판정을 검증합니다:
|
|
* - base == theirs (코어 미변경) → 적용 목록 제외 (사용자 상태 보존)
|
|
* - base 없음 (신규) → added 로 목록 포함
|
|
* - size 다름 / md5 다름 (변경) → changed 로 목록 포함
|
|
* - symlink / excludes / protected → 목록 제외
|
|
*/
|
|
class CoreBackupHelperApplyListTest extends TestCase
|
|
{
|
|
private string $basePath; // 백업 스냅샷 (base = 구버전 원본)
|
|
|
|
private string $theirsPath; // _pending (theirs = 신 버전)
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$root = storage_path('app/test_apply_list_'.uniqid());
|
|
$this->basePath = $root.DIRECTORY_SEPARATOR.'base';
|
|
$this->theirsPath = $root.DIRECTORY_SEPARATOR.'theirs';
|
|
|
|
File::ensureDirectoryExists($this->basePath);
|
|
File::ensureDirectoryExists($this->theirsPath);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
$root = dirname($this->basePath);
|
|
if (File::isDirectory($root)) {
|
|
File::deleteDirectory($root);
|
|
}
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* base == theirs 인 파일은 적용 목록에서 제외됩니다 (코어 미변경 → 보존).
|
|
*/
|
|
public function test_unchanged_file_is_excluded_from_apply_list(): void
|
|
{
|
|
$this->writeBoth('app/Same.php', '<?php // identical', '<?php // identical');
|
|
|
|
$result = CoreBackupHelper::computeApplyList(
|
|
$this->basePath,
|
|
$this->theirsPath,
|
|
['app'],
|
|
[],
|
|
[],
|
|
);
|
|
|
|
$this->assertNotContains('app/Same.php', $result['apply']);
|
|
$this->assertSame(0, $result['added_count']);
|
|
$this->assertSame(0, $result['changed_count']);
|
|
}
|
|
|
|
/**
|
|
* base 에 없고 theirs 에만 있는 파일은 added 로 포함됩니다 (신규).
|
|
*/
|
|
public function test_new_file_is_added_to_apply_list(): void
|
|
{
|
|
File::ensureDirectoryExists($this->theirsPath.DIRECTORY_SEPARATOR.'app');
|
|
File::put($this->theirsPath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'New.php', '<?php // new');
|
|
|
|
$result = CoreBackupHelper::computeApplyList(
|
|
$this->basePath,
|
|
$this->theirsPath,
|
|
['app'],
|
|
[],
|
|
[],
|
|
);
|
|
|
|
$this->assertContains('app/New.php', $result['apply']);
|
|
$this->assertSame(1, $result['added_count']);
|
|
$this->assertSame(0, $result['changed_count']);
|
|
}
|
|
|
|
/**
|
|
* size 가 다른 파일은 md5 계산 없이 changed 로 포함됩니다.
|
|
*/
|
|
public function test_size_differing_file_is_changed(): void
|
|
{
|
|
$this->writeBoth('app/Grown.php', '<?php // small', '<?php // this content is clearly longer than base');
|
|
|
|
$result = CoreBackupHelper::computeApplyList(
|
|
$this->basePath,
|
|
$this->theirsPath,
|
|
['app'],
|
|
[],
|
|
[],
|
|
);
|
|
|
|
$this->assertContains('app/Grown.php', $result['apply']);
|
|
$this->assertSame(1, $result['changed_count']);
|
|
$this->assertSame(0, $result['added_count']);
|
|
}
|
|
|
|
/**
|
|
* size 는 같지만 내용(md5)이 다른 파일은 changed 로 포함됩니다.
|
|
*/
|
|
public function test_same_size_different_content_file_is_changed(): void
|
|
{
|
|
// 동일 길이(12), 내용만 다름
|
|
$this->writeBoth('app/Edited.php', 'AAAAAAAAAAAA', 'BBBBBBBBBBBB');
|
|
|
|
$result = CoreBackupHelper::computeApplyList(
|
|
$this->basePath,
|
|
$this->theirsPath,
|
|
['app'],
|
|
[],
|
|
[],
|
|
);
|
|
|
|
$this->assertContains('app/Edited.php', $result['apply']);
|
|
$this->assertSame(1, $result['changed_count']);
|
|
}
|
|
|
|
/**
|
|
* excludes 패턴에 매칭되는 파일은 목록에서 제외됩니다.
|
|
*/
|
|
public function test_excluded_file_is_not_in_apply_list(): void
|
|
{
|
|
// theirs 에만 존재하지만 node_modules 하위 → 제외
|
|
File::ensureDirectoryExists($this->theirsPath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'node_modules');
|
|
File::put($this->theirsPath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'node_modules'.DIRECTORY_SEPARATOR.'x.js', 'x');
|
|
|
|
$result = CoreBackupHelper::computeApplyList(
|
|
$this->basePath,
|
|
$this->theirsPath,
|
|
['app'],
|
|
[],
|
|
['node_modules'],
|
|
);
|
|
|
|
$this->assertSame([], $result['apply']);
|
|
}
|
|
|
|
/**
|
|
* protected_paths 하위 항목은 목록에서 제외됩니다.
|
|
*/
|
|
public function test_protected_path_file_is_not_in_apply_list(): void
|
|
{
|
|
File::ensureDirectoryExists($this->theirsPath.DIRECTORY_SEPARATOR.'storage'.DIRECTORY_SEPARATOR.'app');
|
|
File::put($this->theirsPath.DIRECTORY_SEPARATOR.'storage'.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'runtime.txt', 'data');
|
|
|
|
$result = CoreBackupHelper::computeApplyList(
|
|
$this->basePath,
|
|
$this->theirsPath,
|
|
['storage'],
|
|
['storage'],
|
|
[],
|
|
);
|
|
|
|
$this->assertSame([], $result['apply']);
|
|
}
|
|
|
|
/**
|
|
* added 와 changed 가 섞여 있을 때 apply 목록이 정렬되어 둘 다 포함합니다.
|
|
*/
|
|
public function test_apply_list_merges_added_and_changed_sorted(): void
|
|
{
|
|
$this->writeBoth('app/Zeta.php', 'old', 'new-different'); // changed
|
|
File::put($this->theirsPath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'Alpha.php', 'brand-new'); // added
|
|
$this->writeBoth('app/Keep.php', 'same', 'same'); // unchanged → 제외
|
|
|
|
$result = CoreBackupHelper::computeApplyList(
|
|
$this->basePath,
|
|
$this->theirsPath,
|
|
['app'],
|
|
[],
|
|
[],
|
|
);
|
|
|
|
$this->assertSame(['app/Alpha.php', 'app/Zeta.php'], $result['apply']);
|
|
$this->assertSame(1, $result['added_count']);
|
|
$this->assertSame(1, $result['changed_count']);
|
|
}
|
|
|
|
/**
|
|
* 단일 파일 target 도 3-way 판정을 받습니다 (변경 시 changed).
|
|
*/
|
|
public function test_single_file_target_is_classified(): void
|
|
{
|
|
File::put($this->basePath.DIRECTORY_SEPARATOR.'artisan', 'v1');
|
|
File::put($this->theirsPath.DIRECTORY_SEPARATOR.'artisan', 'v2-changed');
|
|
|
|
$result = CoreBackupHelper::computeApplyList(
|
|
$this->basePath,
|
|
$this->theirsPath,
|
|
['artisan'],
|
|
[],
|
|
[],
|
|
);
|
|
|
|
$this->assertContains('artisan', $result['apply']);
|
|
$this->assertSame(1, $result['changed_count']);
|
|
}
|
|
|
|
/**
|
|
* theirs 의 symlink 는 적용 목록에서 제외되고 has_symlink 플래그가 켜집니다.
|
|
* (링크 자체는 copyDirectory 의 별도 링크 처리 경로가 담당 — 목록 산출 대상 아님)
|
|
*/
|
|
public function test_symlink_is_excluded_and_flagged(): void
|
|
{
|
|
// theirs/app/real.php + theirs/app/link.php(→ real.php) 구성
|
|
File::ensureDirectoryExists($this->theirsPath.DIRECTORY_SEPARATOR.'app');
|
|
$realTarget = $this->theirsPath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'real.php';
|
|
File::put($realTarget, '<?php // real');
|
|
|
|
$linkPath = $this->theirsPath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'link.php';
|
|
if (! @symlink($realTarget, $linkPath) || ! is_link($linkPath)) {
|
|
// Windows SeCreateSymbolicLink 권한 부족 등 — 링크 생성 불가 환경은 skip
|
|
$this->markTestSkipped('symlink 생성 불가 환경 (권한 부족)');
|
|
}
|
|
|
|
$result = CoreBackupHelper::computeApplyList(
|
|
$this->basePath,
|
|
$this->theirsPath,
|
|
['app'],
|
|
[],
|
|
[],
|
|
);
|
|
|
|
// real.php 는 added 로 포함되지만 link.php 는 목록에서 제외
|
|
$this->assertContains('app/real.php', $result['apply']);
|
|
$this->assertNotContains('app/link.php', $result['apply']);
|
|
$this->assertTrue($result['has_symlink'], 'symlink 발견 시 has_symlink 플래그가 켜져야 한다');
|
|
}
|
|
|
|
/**
|
|
* base·theirs 양쪽에 동일 파일을 씁니다.
|
|
*/
|
|
private function writeBoth(string $relative, string $baseContent, string $theirsContent): void
|
|
{
|
|
$rel = str_replace('/', DIRECTORY_SEPARATOR, $relative);
|
|
|
|
$baseFile = $this->basePath.DIRECTORY_SEPARATOR.$rel;
|
|
File::ensureDirectoryExists(dirname($baseFile));
|
|
File::put($baseFile, $baseContent);
|
|
|
|
$theirsFile = $this->theirsPath.DIRECTORY_SEPARATOR.$rel;
|
|
File::ensureDirectoryExists(dirname($theirsFile));
|
|
File::put($theirsFile, $theirsContent);
|
|
}
|
|
}
|