Files
XC_VM/tests/Unit/BootPipelineTest.php
T
Divarion_D e1bb80672e refactor(bootstrap): split XC_Bootstrap into an injectable BootKernel pipeline
Replace the fully-static XC_Bootstrap god-class with a stage pipeline so the boot
logic becomes unit-testable and the per-context sequences are explicit.

- BootState replaces the 8 static readiness flags with a value object threaded
  through the pipeline; stages read/write it instead of static state.
- BootStageInterface + BootPipeline run an ordered stage list and abort loudly
  on a throwing stage.
- 16 stages under Core/Bootstrap/Stage/ hold one subsystem each, extracted
  verbatim from the old private methods (constants, config, flood, host, session,
  database, legacy core, redis, process title, admin API, translator, admin
  shutdown, status constants, admin globals, container populate, health check).
- StageProfiles builds the ordered list per context, mirroring the exact previous
  sequence; BootKernel resolves options, sets up the container and runs it.
- XC_Bootstrap is now a thin BC facade delegating to BootKernel; its getters read
  the returned BootState. reset() also clears EventDispatcher and the new
  DatabaseFactory::reset() (a side-effect-free registry clear for test isolation).

The DB-touching contexts (Cli/Stream/Admin) still require a live MySQL and the
xcvm_core extension, so they are verified on a canary rather than in CI; the
Minimal context and the pipeline/profile composition are covered by new tests.
2026-09-16 16:58:49 +03:00

163 lines
5.2 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
use XcVm\Core\Bootstrap\BootKernel;
use XcVm\Core\Bootstrap\BootPipeline;
use XcVm\Core\Bootstrap\BootStageInterface;
use XcVm\Core\Bootstrap\BootState;
use XcVm\Core\Bootstrap\StageProfiles;
use XcVm\Core\Bootstrap\Stage\AdminApiStage;
use XcVm\Core\Bootstrap\Stage\AdminGlobalsStage;
use XcVm\Core\Bootstrap\Stage\AdminShutdownStage;
use XcVm\Core\Bootstrap\Stage\ConfigStage;
use XcVm\Core\Bootstrap\Stage\ConstantsStage;
use XcVm\Core\Bootstrap\Stage\ContainerPopulateStage;
use XcVm\Core\Bootstrap\Stage\DatabaseStage;
use XcVm\Core\Bootstrap\Stage\FloodProtectionStage;
use XcVm\Core\Bootstrap\Stage\HealthCheckStage;
use XcVm\Core\Bootstrap\Stage\HostVerificationStage;
use XcVm\Core\Bootstrap\Stage\LegacyCoreStage;
use XcVm\Core\Bootstrap\Stage\ProcessTitleStage;
use XcVm\Core\Bootstrap\Stage\RedisStage;
use XcVm\Core\Bootstrap\Stage\SessionStage;
use XcVm\Core\Bootstrap\Stage\StatusConstantsStage;
use XcVm\Core\Bootstrap\Stage\TranslatorStage;
use XcVm\Core\Container\ServiceContainer;
use XcVm\Core\Enum\BootContext;
/**
* Pipeline mechanics and per-context profile composition. A full boot cannot run
* in-process (ConfigStage calls the \XC_VM config extension, absent in tests), so
* this covers ordering and composition with fakes/pure inspection.
*/
final class BootPipelineTest extends TestCase {
protected function tearDown(): void {
ServiceContainer::resetInstance();
}
private function state(BootContext $ctx = BootContext::Minimal): BootState {
return new BootState($ctx, [], ServiceContainer::getInstance());
}
private function recordingStage(string $name, ArrayObject $log): BootStageInterface {
return new class ($name, $log) implements BootStageInterface {
public function __construct(private string $name, private ArrayObject $log) {
}
public function run(BootState $state): void {
$this->log->append($this->name);
}
};
}
public function testPipelineRunsStagesInOrder(): void {
$log = new ArrayObject();
(new BootPipeline([
$this->recordingStage('a', $log),
$this->recordingStage('b', $log),
$this->recordingStage('c', $log),
]))->run($this->state());
$this->assertSame(['a', 'b', 'c'], $log->getArrayCopy());
}
public function testPipelineAbortsOnThrow(): void {
$log = new ArrayObject();
$boom = new class implements BootStageInterface {
public function run(BootState $state): void {
throw new RuntimeException('boom');
}
};
$this->expectException(RuntimeException::class);
try {
(new BootPipeline([$boom, $this->recordingStage('after', $log)]))->run($this->state());
} finally {
$this->assertNotContains('after', $log->getArrayCopy(), 'stage after a throwing stage must not run');
}
}
/**
* @param array<string,mixed> $options
* @return list<class-string>
*/
private function classesFor(BootContext $ctx, array $options = []): array {
$opts = array_merge(BootKernel::defaults($ctx), $options);
return array_map('get_class', StageProfiles::for($ctx, $opts));
}
public function testMinimalProfile(): void {
$this->assertSame([
ConstantsStage::class,
ConfigStage::class,
FloodProtectionStage::class,
HostVerificationStage::class,
ContainerPopulateStage::class,
], $this->classesFor(BootContext::Minimal));
}
public function testCliProfileDefaultsOmitRedisAndProcessTitle(): void {
$this->assertSame([
ConstantsStage::class,
ConfigStage::class,
FloodProtectionStage::class,
HostVerificationStage::class,
DatabaseStage::class,
LegacyCoreStage::class,
ContainerPopulateStage::class,
HealthCheckStage::class,
], $this->classesFor(BootContext::Cli));
}
public function testCliProfileAddsRedisAndProcessTitleWhenRequested(): void {
$classes = $this->classesFor(BootContext::Cli, ['redis' => true, 'process' => 'worker']);
$this->assertContains(RedisStage::class, $classes);
$this->assertContains(ProcessTitleStage::class, $classes);
}
public function testStreamProfileIsDatabaseOnly(): void {
$this->assertSame([
ConstantsStage::class,
ConfigStage::class,
FloodProtectionStage::class,
HostVerificationStage::class,
DatabaseStage::class,
ContainerPopulateStage::class,
HealthCheckStage::class,
], $this->classesFor(BootContext::Stream));
}
public function testAdminProfileFullSequence(): void {
$this->assertSame([
ConstantsStage::class,
ConfigStage::class,
FloodProtectionStage::class,
HostVerificationStage::class,
SessionStage::class,
DatabaseStage::class,
LegacyCoreStage::class,
RedisStage::class,
AdminApiStage::class,
TranslatorStage::class,
AdminShutdownStage::class,
StatusConstantsStage::class,
AdminGlobalsStage::class,
ContainerPopulateStage::class,
HealthCheckStage::class,
], $this->classesFor(BootContext::Admin));
}
public function testMinimalHasNoHealthCheck(): void {
$this->assertNotContains(HealthCheckStage::class, $this->classesFor(BootContext::Minimal));
}
public function testDefaultsPerContext(): void {
$this->assertTrue(BootKernel::defaults(BootContext::Admin)['redis']);
$this->assertFalse(BootKernel::defaults(BootContext::Cli)['redis']);
$this->assertTrue(BootKernel::defaults(BootContext::Stream)['cached']);
$this->assertFalse(BootKernel::defaults(BootContext::Admin)['cached']);
}
}