Files
Gnuboard7/tests/Unit/Seo/SitemapProgressTest.php
T
HeuJung 3dd34104cc feat(seo): 사이트맵 대용량 하네스 + 코어 7.1.0 마감
S6(하네스 + 마감): 큐/생성기의 볼륨 붕괴 재발을 정적으로 강제하고,
S1~S5 사이트맵 개편의 공개 표면을 문서화하며 코어를 7.1.0 으로 확정한다.

하네스(주안점 5):
- test-scenario 매니페스트에 scale 축(n≥100000 + assert) 도입.
 내장 YAML fallback 파서 제약으로 block-array of flow objects 형태.
- 룰 2종: scenario-scale-axis-required(batch 매니페스트 scale 강제),
 job-generator-needs-scale-test(Jobs·*Generator 변경 시 scale 시나리오 요구).
 *Command 은 볼륨 비의존이라 제외 — tags:[batch] opt-in 으로 커버.
- tests/scenarios/sitemap-generation.yaml + @scale 마킹 3(Writer/Progress/ManagerMode).

문서: seo-system(증분 저장소·모드·진행상황·lazy contributor)·testing-guide(scale 절)·
broadcasting(sitemap 채널)·hooks(2훅)·database-guide(loc_hash)·cheatsheet.

버전: 코어 7.0.5→7.1.0(config+env+README+INSTALL+CHANGELOG) +
모듈 board/ecommerce/page requires.g7_version >=7.1.0.
2026-07-18 20:56:47 +09:00

168 lines
5.4 KiB
PHP

<?php
namespace Tests\Unit\Seo;
use App\Events\GenericBroadcastEvent;
use App\Seo\SitemapProgress;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
/**
* SitemapProgress 진행상황 스토어 테스트
*
* 상태 전이(queued→running→writing→completed|failed), 캐시 기록, 방송(Reverb ON),
* 방송 스로틀(N URL 간격), Reverb OFF 시 캐시 폴백을 검증합니다.
*/
class SitemapProgressTest extends TestCase
{
private SitemapProgress $progress;
protected function setUp(): void
{
parent::setUp();
$this->progress = app(SitemapProgress::class);
}
/**
* Reverb 방송이 실제로 발생하도록 드라이버/호스트를 설정합니다.
*/
private function enableReverb(): void
{
config(['broadcasting.default' => 'reverb']);
config(['broadcasting.connections.reverb.options.host' => 'localhost']);
}
/**
* 실행 이력이 없으면 get() 은 null 을 반환한다.
*/
public function test_get_returns_null_when_no_run(): void
{
$this->assertNull($this->progress->get());
}
/**
* start() 는 status=queued 와 mode/started_at 을 기록한다.
*/
public function test_start_records_queued_state(): void
{
$this->progress->start('full');
$state = $this->progress->get();
$this->assertSame('queued', $state['status']);
$this->assertSame('full', $state['mode']);
$this->assertNotNull($state['started_at']);
$this->assertSame(0, $state['urls']);
}
/**
* phase() 는 status=running 과 현재 단계를 기록한다.
*/
public function test_phase_records_running_state(): void
{
$this->progress->start('full');
$this->progress->phase('sirsoft-board', 1200);
$state = $this->progress->get();
$this->assertSame('running', $state['status']);
$this->assertSame('sirsoft-board', $state['phase']);
$this->assertSame(1200, $state['urls']);
}
/**
* writing() 은 status=writing 을 기록한다.
*/
public function test_writing_records_writing_state(): void
{
$this->progress->start('full');
$this->progress->writing(3400);
$state = $this->progress->get();
$this->assertSame('writing', $state['status']);
$this->assertSame(3400, $state['urls']);
}
/**
* complete() 는 status=completed 와 url_count/child_count 를 기록한다.
*/
public function test_complete_records_completed_state(): void
{
$this->progress->start('full');
$this->progress->complete(['url_count' => 5000, 'child_count' => 2]);
$state = $this->progress->get();
$this->assertSame('completed', $state['status']);
$this->assertSame(5000, $state['url_count']);
$this->assertSame(2, $state['child_count']);
$this->assertNotNull($state['finished_at']);
}
/**
* fail() 은 status=failed 와 메시지를 기록한다.
*/
public function test_fail_records_failed_state(): void
{
$this->progress->start('full');
$this->progress->fail('boom');
$state = $this->progress->get();
$this->assertSame('failed', $state['status']);
$this->assertSame('boom', $state['message']);
$this->assertNotNull($state['finished_at']);
}
/**
* Reverb ON 이면 방송 payload 가 상태 API data 형태(progress + realtime_enabled)와 동형이다.
*/
public function test_broadcast_payload_mirrors_status_shape(): void
{
$this->enableReverb();
Event::fake([GenericBroadcastEvent::class]);
$this->progress->start('full');
Event::assertDispatched(GenericBroadcastEvent::class, function ($event) {
return $event->channel === 'core.admin.seo.sitemap'
&& $event->eventName === 'sitemap.progress.updated'
&& ($event->payload['realtime_enabled'] ?? null) === true
&& ($event->payload['progress']['status'] ?? null) === 'queued';
});
}
/**
* Reverb OFF 여도 캐시는 기록된다(폴링 폴백 성립).
*/
public function test_cache_is_written_even_when_reverb_off(): void
{
config(['broadcasting.default' => 'null']);
Event::fake([GenericBroadcastEvent::class]);
$this->progress->start('full');
Event::assertNotDispatched(GenericBroadcastEvent::class);
$this->assertSame('queued', $this->progress->get()['status']);
}
/**
* 같은 단계에서 urls 만 증가하면 방송은 간격 스로틀되지만 캐시는 항상 기록된다.
*
* @scale n=1500000 asserts=progress_broadcast_throttled
*/
public function test_phase_broadcast_is_throttled_but_cache_always_written(): void
{
$this->enableReverb();
Event::fake([GenericBroadcastEvent::class]);
// 단계 진입 — 방송 1회
$this->progress->phase('sirsoft-board', 100);
// 같은 단계, 소폭 증가(<5000) — 방송 스킵, 캐시는 갱신
$this->progress->phase('sirsoft-board', 200);
// 같은 단계, 간격 초과(>=5000) — 방송 재발
$this->progress->phase('sirsoft-board', 5300);
// 방송은 진입 + 간격초과 = 2회
Event::assertDispatchedTimes(GenericBroadcastEvent::class, 2);
// 캐시는 마지막 값으로 항상 갱신
$this->assertSame(5300, $this->progress->get()['urls']);
}
}