From ba5f39f934aa292c3dcaa1aee3f1b11e3eaf1ced Mon Sep 17 00:00:00 2001 From: Josiah King Date: Fri, 21 Aug 2026 17:00:36 +0100 Subject: [PATCH] Phase 5.7: add testing SDK and phase acceptance --- CHANGELOG.md | 1 + packages/README.md | 8 +- packages/testing/README.md | 32 +++ packages/testing/src/.gitkeep | 0 .../Component/ComponentDefinitionFixture.php | 62 ++++++ .../Component/ComponentEntryPointFixture.php | 51 +++++ .../Phase5ComponentAcceptanceTest.php | 204 ++++++++++++++++++ .../ComponentDefinitionFixtureTest.php | 170 +++++++++++++++ .../ComponentEntryPointFixtureTest.php | 178 +++++++++++++++ .../EvolvePhp2PackageSkeletonTest.php | 7 +- 10 files changed, 708 insertions(+), 5 deletions(-) delete mode 100644 packages/testing/src/.gitkeep create mode 100644 packages/testing/src/Component/ComponentDefinitionFixture.php create mode 100644 packages/testing/src/Component/ComponentEntryPointFixture.php create mode 100644 packages/testing/tests/Integration/Phase5ComponentAcceptanceTest.php create mode 100644 packages/testing/tests/Unit/Component/ComponentDefinitionFixtureTest.php create mode 100644 packages/testing/tests/Unit/Component/ComponentEntryPointFixtureTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 54933b3..e0bd6c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Component foundation +- Added Phase 5.7 Testing SDK and independent component acceptance coverage inside `evolvephp/testing`, including public pre-beta experimental `ComponentDefinitionFixture` and `ComponentEntryPointFixture` closure-backed fixtures for the accepted component contracts, no direct PHPUnit dependency in Testing source, no Module/Plugin replacement semantics, no hidden registries or lifecycle orchestration, and independent integration coverage for explicit `ModuleDefinition`, `ComposerPluginDiscovery`, app-controlled enablement, Core `ComponentBootstrapper`, real graph ordering, restricted registration, service-registry freeze, `ApplicationKernel` boot, component boot, ready and reverse shutdown. Create-project tooling, generators, Doctor, route inspection, environment inspection and broader developer tooling remain Phase 6 work. - Added Phase 5.6B Composer plugin discovery inside `evolvephp/plugin`, including public pre-beta experimental `ComposerPluginDiscovery` for Composer 2 `vendor/composer/installed.json`, schema 1 `extra.evolvephp.plugin` metadata, Composer package name as the authoritative identifier, deterministic discovered-definition ordering, graph metadata projection into existing Contracts declarations, application-controlled enablement through `evolve.components.enabled`, disabled-definition inertness, no entry-point construction or instantiation during discovery, no recursive package scanning, no Composer 1 support, no new Composer dependency and no Composer runtime API dependency. - Added Phase 5.6A explicit component definitions and app-controlled enablement, including public pre-beta experimental `ComponentDefinition` in `evolvephp/contracts`, explicit `ModuleDefinition` and `PluginDefinition` wrappers, Core-owned `ComponentBootstrapper`, `ApplicationKernel` bootstrapper integration, strict `evolve.components.enabled` allowlist validation, disabled-definition inertness, validate-all-before-create behavior, existing graph-resolution reuse and exact binding into the Phase 5.5 lifecycle coordinator. Composer discovery, package scanning, Composer `extra` metadata, automatic enablement, descriptor serialization, component versions, dependency version ranges and SemVer graph decisions remain deferred. - Added Phase 5.5 component lifecycle entry points and Core-owned orchestration, including public pre-beta experimental `ComponentEntryPoint` and `ComponentBootContext` in `evolvephp/contracts`, `Evolve\Module\Module` and `Evolve\Plugin\Plugin` SDK entry points, deterministic registration -> freeze -> boot -> ready ordering through `ApplicationKernel`, reverse successful-boot shutdown, boot-failure deferred cleanup, ready-failure cleanup, attributed aggregate shutdown failures, original startup failure preservation and at-most-once lifecycle state enforcement. Discovery and enablement remain Phase 5.6. diff --git a/packages/README.md b/packages/README.md index 47d2c7b..c49ed96 100644 --- a/packages/README.md +++ b/packages/README.md @@ -2,7 +2,7 @@ `packages/` contains the initial EvolvePHP 2 modular-monorepo package set. -The packages define Composer package identities, namespace ownership, dependency direction, the first Phase 3 lifecycle, configuration, service-container, execution-scope, runtime-neutral execution orchestration, generic Core instrumentation and minimal Core console foundations, plus the Phase 4.1 PSR HTTP middleware foundation, Phase 4.2 routing foundation, Phase 4.3 routed handler dispatch foundation, Phase 4.4 HTTP execution-kernel integration, Phase 4.5 HTTP response/error and health foundation, Phase 4.6 `ResponseEmitter` runtime-neutral response-emission boundary and integration acceptance, Phase 5.1 Component Identity Foundation, the Phase 5.2 Module/Plugin immutable descriptor foundation, the Phase 5.3A dependency/capability graph declaration vocabulary, the Phase 5.3B Core-owned graph validation/resolution foundation, the Phase 5.4 restricted component service-definition registration foundation, the Phase 5.5 component lifecycle entry-point/orchestration foundation, the Phase 5.6A explicit component definition/bootstrap foundation and the Phase 5.6B Composer plugin discovery foundation for EvolvePHP 2. Complete runtime implementation is not yet present for the framework, and the packages are not yet published. +The packages define Composer package identities, namespace ownership, dependency direction, the first Phase 3 lifecycle, configuration, service-container, execution-scope, runtime-neutral execution orchestration, generic Core instrumentation and minimal Core console foundations, plus the Phase 4.1 PSR HTTP middleware foundation, Phase 4.2 routing foundation, Phase 4.3 routed handler dispatch foundation, Phase 4.4 HTTP execution-kernel integration, Phase 4.5 HTTP response/error and health foundation, Phase 4.6 `ResponseEmitter` runtime-neutral response-emission boundary and integration acceptance, Phase 5.1 Component Identity Foundation, the Phase 5.2 Module/Plugin immutable descriptor foundation, the Phase 5.3A dependency/capability graph declaration vocabulary, the Phase 5.3B Core-owned graph validation/resolution foundation, the Phase 5.4 restricted component service-definition registration foundation, the Phase 5.5 component lifecycle entry-point/orchestration foundation, the Phase 5.6A explicit component definition/bootstrap foundation, the Phase 5.6B Composer plugin discovery foundation and the Phase 5.7 Testing component fixture and independent acceptance foundation for EvolvePHP 2. Complete runtime implementation is not yet present for the framework, and the packages are not yet published. All package manifests require PHP `^8.4`. @@ -15,7 +15,7 @@ All package manifests require PHP `^8.4`. | `evolvephp/http` | `Evolve\Http\` | HTTP boundary with the Phase 4.1 PSR HTTP interoperability and `MiddlewarePipeline` foundation, Phase 4.2 route definitions and matching, Phase 4.3 routed handler dispatch with typed routing failures, Phase 4.4 `HttpKernel` integration with Core execution orchestration, Phase 4.5 response/error plus health foundation and Phase 4.6 explicit response-emitter boundary; runtime adapters remain deferred. | | `evolvephp/module` | `Evolve\Module\` | Module SDK boundary with the public experimental `ModuleDescriptor`, EvolvePHP-major compatibility validation foundation, Phase 5.3A `graphDeclaration()` projection, Phase 5.5 `Module` entry point extending `ComponentEntryPoint` and Phase 5.6A explicit `ModuleDefinition`; discovery remains deferred. | | `evolvephp/plugin` | `Evolve\Plugin\` | Plugin SDK boundary with the public experimental `PluginDescriptor`, EvolvePHP-major compatibility validation foundation, Phase 5.3A `graphDeclaration()` projection, Phase 5.5 `Plugin` entry point extending `ComponentEntryPoint`, Phase 5.6A explicit `PluginDefinition` and Phase 5.6B `ComposerPluginDiscovery` for packaged plugin metadata. | -| `evolvephp/testing` | `Evolve\Testing\` | Testing-support boundary for later developer testing utilities. | +| `evolvephp/testing` | `Evolve\Testing\` | Development-only testing-support boundary, including Phase 5.7 public experimental component fixtures and independent Phase 5 acceptance coverage. | ## Dependency Direction @@ -78,6 +78,8 @@ Phase 5.6A adds explicit component definitions and app-controlled enablement. Co Phase 5.6B adds packaged plugin metadata discovery inside `evolvephp/plugin`. The public experimental `ComposerPluginDiscovery` reads Composer 2 `vendor/composer/installed.json` only, recognizes `extra.evolvephp.plugin`, treats the Composer package name as the authoritative identifier, returns sorted `PluginDefinition` objects and keeps application-controlled enablement through `evolve.components.enabled`. Disabled discovered definitions remain inert; discovery does not instantiate entry points, check classes, inspect root composer.json modules, use Composer 1 metadata, access the network, add caching, or require the Composer runtime API. It uses installed.json only with no recursive package scanning and no new Composer dependency. -The Phase 4 HTTP package foundation is complete after this slice, but the complete production runtime is still deferred. EvolvePHP 2 does not yet provide complete HTTP runtime adapters, concrete runtime adapters, concrete PSR-7 implementation, SAPI response transmission, SAPI request creation, HTML or JSON error rendering, content negotiation, debug pages, automatic health routes, environment or dotenv loading, configuration files, queue or scheduled-job adapters, retry policy, process recycling or termination, module/plugin runtime, runtime CLI adapters, shell executables, argv or shell parsing, stdout/stderr stream implementations, Symfony Console integration, Doctor, generators, developer tooling commands, Insight, Observe, OpenTelemetry propagation, tracing, metrics, logs, telemetry storage/export, persistent-worker concurrency guarantees or production-ready framework runtime behavior. Phase 5.6B does not implement descriptor serialization, component versions, dependency version ranges or Composer semantic-version constraint evaluation. Module runtime remains deferred to later Phase 5 slices, and Phase 6 owns developer tooling. +Phase 5.7 adds the first concrete `evolvephp/testing` SDK surface for component tests. `ComponentDefinitionFixture` and `ComponentEntryPointFixture` are public experimental test fixtures that reuse the real component contracts while remaining simple closure-backed wrappers. The Testing package also carries an independent integration acceptance test covering explicit `ModuleDefinition`, `ComposerPluginDiscovery`, application-controlled enablement, Core `ComponentBootstrapper`, real dependency ordering, restricted registration, `ServiceRegistry` freeze, `ApplicationKernel` boot, component boot, ready and reverse shutdown. These fixtures do not replace Module or Plugin definitions, lifecycle orchestration, discovery, graph resolution, developer tooling, mocks or PHPUnit assertion APIs. + +The Phase 4 HTTP package foundation is complete after this slice, but the complete production runtime is still deferred. EvolvePHP 2 does not yet provide complete HTTP runtime adapters, concrete runtime adapters, concrete PSR-7 implementation, SAPI response transmission, SAPI request creation, HTML or JSON error rendering, content negotiation, debug pages, automatic health routes, environment or dotenv loading, configuration files, queue or scheduled-job adapters, retry policy, process recycling or termination, module/plugin runtime, runtime CLI adapters, shell executables, argv or shell parsing, stdout/stderr stream implementations, Symfony Console integration, Doctor, generators, developer tooling commands, Insight, Observe, OpenTelemetry propagation, tracing, metrics, logs, telemetry storage/export, persistent-worker concurrency guarantees or production-ready framework runtime behavior. These capabilities remain deferred. Phase 5.6B does not implement descriptor serialization, component versions, dependency version ranges or Composer semantic-version constraint evaluation. Phase 5.7 does not add create-project tooling, generators, Doctor, environment inspection, route inspection or a general testing framework. Phase 6 owns developer tooling. The EvolvePHP 2 repository root resolves and validates these local packages. See [../DEVELOPMENT.md](../DEVELOPMENT.md) for setup, testing, quality commands, lockfile, static-analysis, coding-standard and architecture-boundary policy. diff --git a/packages/testing/README.md b/packages/testing/README.md index 44313fd..1943524 100644 --- a/packages/testing/README.md +++ b/packages/testing/README.md @@ -2,6 +2,8 @@ Testing utilities for EvolvePHP 2 packages and applications. +This package remains development-only. + ## Package `evolvephp/testing` @@ -14,6 +16,36 @@ PHP `^8.4` `evolvephp/contracts`, `evolvephp/core`, `evolvephp/http`, `evolvephp/module`, `evolvephp/plugin` +## Component Fixtures + +Phase 5.7 adds the first small Testing SDK surface for component-oriented tests. + +The package provides two public experimental fixtures: + +- `Evolve\Testing\Component\ComponentDefinitionFixture` +- `Evolve\Testing\Component\ComponentEntryPointFixture` + +Both fixtures reuse the real Evolve component contracts from `evolvephp/contracts`. +They are intended for framework and application tests that need a generic component +definition or lifecycle participant without creating bespoke test-only classes each +time. + +`ComponentDefinitionFixture` preserves a caller-supplied component identifier, +component type, graph relations, optional validator and entry-point factory. It +creates one graph declaration during construction, returns that same declaration +on every `graphDeclaration()` call, invokes the validator only when supplied and +delegates entry-point creation to the caller-supplied factory. + +`ComponentEntryPointFixture` accepts optional callbacks for `register`, `boot`, +`ready` and `shutdown`. Missing callbacks are no-ops, supplied callbacks receive +the exact lifecycle objects passed by the framework and callback failures are not +wrapped by the fixture. + +These APIs are experimental and may change before stable release. They do not +replace `ModuleDefinition`, `PluginDefinition`, module or plugin entry-point +interfaces, graph resolution, lifecycle orchestration, service registration +policy, discovery, developer tooling, mocks or PHPUnit assertions. + ## Publication Status EvolvePHP 2 is pre-release. This package is not yet independently published, and the current canonical source is the EvolvePHP monorepo: diff --git a/packages/testing/src/.gitkeep b/packages/testing/src/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/testing/src/Component/ComponentDefinitionFixture.php b/packages/testing/src/Component/ComponentDefinitionFixture.php new file mode 100644 index 0000000..4fb33a9 --- /dev/null +++ b/packages/testing/src/Component/ComponentDefinitionFixture.php @@ -0,0 +1,62 @@ +graphDeclaration = new ComponentGraphDeclaration($identifier, $relations); + } + + public function identifier(): ComponentIdentifier + { + return $this->identifier; + } + + public function type(): ComponentType + { + return $this->type; + } + + public function graphDeclaration(): ComponentGraphDeclaration + { + return $this->graphDeclaration; + } + + public function validate(): void + { + ($this->validator ?? static function (): void {})(); + } + + public function createEntryPoint(): ComponentEntryPoint + { + return ($this->entryPointFactory)(); + } +} diff --git a/packages/testing/src/Component/ComponentEntryPointFixture.php b/packages/testing/src/Component/ComponentEntryPointFixture.php new file mode 100644 index 0000000..3d9b4aa --- /dev/null +++ b/packages/testing/src/Component/ComponentEntryPointFixture.php @@ -0,0 +1,51 @@ +register ?? static function (ServiceDefinitionRegistrar $registrar): void {})($registrar); + } + + public function boot(ComponentBootContext $context): void + { + ($this->boot ?? static function (ComponentBootContext $context): void {})($context); + } + + public function ready(): void + { + ($this->ready ?? static function (): void {})(); + } + + public function shutdown(): void + { + ($this->shutdown ?? static function (): void {})(); + } +} diff --git a/packages/testing/tests/Integration/Phase5ComponentAcceptanceTest.php b/packages/testing/tests/Integration/Phase5ComponentAcceptanceTest.php new file mode 100644 index 0000000..d607ec3 --- /dev/null +++ b/packages/testing/tests/Integration/Phase5ComponentAcceptanceTest.php @@ -0,0 +1,204 @@ + */ + private array $temporaryFiles = []; + + protected function setUp(): void + { + Phase5AcceptanceState::reset(); + } + + protected function tearDown(): void + { + Phase5AcceptanceState::reset(); + + foreach ($this->temporaryFiles as $temporaryFile) { + if (is_file($temporaryFile)) { + unlink($temporaryFile); + } + } + } + + public function test_phase5_component_path_uses_real_discovery_bootstrap_graph_registration_boot_ready_and_shutdown(): void + { + $installedJson = $this->writeInstalledJson([ + [ + 'name' => 'vendor/acceptance-plugin', + 'extra' => [ + 'evolvephp' => [ + 'plugin' => [ + 'schema' => 1, + 'type' => 'plugin', + 'name' => 'Acceptance Plugin', + 'evolve_major' => 2, + 'entry_point' => Phase5AcceptancePlugin::class, + 'graph' => [ + 'dependencies' => [ + [ + 'component' => 'app/acceptance-module', + 'kind' => 'required', + ], + ], + ], + ], + ], + ], + ], + ]); + + $moduleDefinition = new ModuleDefinition( + new ModuleDescriptor( + new \Evolve\Contracts\Component\ComponentIdentifier('app/acceptance-module'), + 'Acceptance Module', + 2, + ), + Phase5AcceptanceModule::class, + ); + $pluginDefinitions = (new ComposerPluginDiscovery($installedJson))->discover(); + $bootstrapper = new ComponentBootstrapper([$moduleDefinition, ...$pluginDefinitions]); + $kernel = new ApplicationKernel( + new ArrayConfiguration([ + 'evolve' => [ + 'components' => [ + 'enabled' => [ + 'vendor/acceptance-plugin', + 'app/acceptance-module', + ], + ], + ], + ]), + services: new ServiceRegistry(), + components: $bootstrapper, + ); + + $kernel->boot(); + + self::assertSame(['registered-value'], Phase5AcceptanceState::$pluginBootValues); + self::assertSame( + [ + 'module:register', + 'plugin:register', + 'module:boot', + 'plugin:boot', + 'module:ready', + 'plugin:ready', + ], + Phase5AcceptanceState::$events, + ); + + $kernel->shutdown(); + + self::assertSame( + [ + 'module:register', + 'plugin:register', + 'module:boot', + 'plugin:boot', + 'module:ready', + 'plugin:ready', + 'plugin:shutdown', + 'module:shutdown', + ], + Phase5AcceptanceState::$events, + ); + self::assertSame(ComponentDependencyKind::Required, $pluginDefinitions[0]->graphDeclaration()->relations()->dependencies()[0]->kind()); + self::assertSame('app/acceptance-module', $pluginDefinitions[0]->graphDeclaration()->relations()->dependencies()[0]->target()->value()); + } + + /** + * @param list> $packages + */ + private function writeInstalledJson(array $packages): string + { + $path = tempnam(sys_get_temp_dir(), 'evolve-phase5-acceptance-'); + + self::assertIsString($path); + file_put_contents($path, json_encode(['packages' => $packages], JSON_THROW_ON_ERROR)); + $this->temporaryFiles[] = $path; + + return $path; + } +} + +final class Phase5AcceptanceState +{ + /** @var list */ + public static array $events = []; + + /** @var list */ + public static array $pluginBootValues = []; + + public static function reset(): void + { + self::$events = []; + self::$pluginBootValues = []; + } +} + +final class Phase5AcceptanceModule implements Module +{ + public function register(ServiceDefinitionRegistrar $registrar): void + { + Phase5AcceptanceState::$events[] = 'module:register'; + $registrar->registerApplication('acceptance.value', static fn(): string => 'registered-value'); + } + + public function boot(ComponentBootContext $context): void + { + Phase5AcceptanceState::$events[] = 'module:boot'; + } + + public function ready(): void + { + Phase5AcceptanceState::$events[] = 'module:ready'; + } + + public function shutdown(): void + { + Phase5AcceptanceState::$events[] = 'module:shutdown'; + } +} + +final class Phase5AcceptancePlugin implements Plugin +{ + public function register(ServiceDefinitionRegistrar $registrar): void + { + Phase5AcceptanceState::$events[] = 'plugin:register'; + } + + public function boot(ComponentBootContext $context): void + { + Phase5AcceptanceState::$events[] = 'plugin:boot'; + Phase5AcceptanceState::$pluginBootValues[] = $context->services()->get('acceptance.value'); + } + + public function ready(): void + { + Phase5AcceptanceState::$events[] = 'plugin:ready'; + } + + public function shutdown(): void + { + Phase5AcceptanceState::$events[] = 'plugin:shutdown'; + } +} diff --git a/packages/testing/tests/Unit/Component/ComponentDefinitionFixtureTest.php b/packages/testing/tests/Unit/Component/ComponentDefinitionFixtureTest.php new file mode 100644 index 0000000..927c07d --- /dev/null +++ b/packages/testing/tests/Unit/Component/ComponentDefinitionFixtureTest.php @@ -0,0 +1,170 @@ +fixture(identifier: $identifier); + + self::assertSame($identifier, $fixture->identifier()); + } + + public function test_component_type_is_returned_exactly(): void + { + $fixture = $this->fixture(type: ComponentType::Plugin); + + self::assertSame(ComponentType::Plugin, $fixture->type()); + } + + public function test_graph_relations_are_represented_from_supplied_identifier_and_relations(): void + { + $identifier = new ComponentIdentifier('app/testing-fixture'); + $dependency = new ComponentDependency( + new ComponentIdentifier('vendor/dependency'), + ComponentDependencyKind::Required, + ); + $capability = new CapabilityIdentifier('testing-capability'); + $relations = new ComponentGraphRelations( + dependencies: [$dependency], + providedCapabilities: [$capability], + ); + $fixture = $this->fixture(identifier: $identifier, relations: $relations); + $declaration = $fixture->graphDeclaration(); + + self::assertSame($identifier, $declaration->identifier()); + self::assertSame($relations, $declaration->relations()); + self::assertSame([$dependency], $declaration->relations()->dependencies()); + self::assertSame([$capability], $declaration->relations()->providedCapabilities()); + } + + public function test_repeated_graph_declaration_calls_return_the_same_object(): void + { + $fixture = $this->fixture(); + + self::assertSame($fixture->graphDeclaration(), $fixture->graphDeclaration()); + } + + public function test_validator_is_optional(): void + { + $this->fixture()->validate(); + + self::addToAssertionCount(1); + } + + public function test_supplied_validator_executes(): void + { + $calls = 0; + $fixture = $this->fixture(validator: static function () use (&$calls): void { + ++$calls; + }); + + $fixture->validate(); + + self::assertSame(1, $calls); + } + + public function test_validator_exceptions_propagate_unchanged(): void + { + $failure = new RuntimeException('validator failed'); + $fixture = $this->fixture(validator: static function () use ($failure): void { + throw $failure; + }); + + try { + $fixture->validate(); + self::fail('Validator failure should propagate.'); + } catch (RuntimeException $exception) { + self::assertSame($failure, $exception); + } + } + + public function test_entry_point_factory_executes_and_returns_the_produced_entry_point(): void + { + $calls = 0; + $entryPoint = new DefinitionFixtureEntryPoint(); + $fixture = $this->fixture(entryPointFactory: static function () use (&$calls, $entryPoint): ComponentEntryPoint { + ++$calls; + + return $entryPoint; + }); + + self::assertSame($entryPoint, $fixture->createEntryPoint()); + self::assertSame(1, $calls); + } + + public function test_entry_point_factory_exceptions_propagate_unchanged(): void + { + $failure = new RuntimeException('factory failed'); + $fixture = $this->fixture(entryPointFactory: static function () use ($failure): ComponentEntryPoint { + throw $failure; + }); + + try { + $fixture->createEntryPoint(); + self::fail('Entry-point factory failure should propagate.'); + } catch (RuntimeException $exception) { + self::assertSame($failure, $exception); + } + } + + public function test_create_entry_point_invokes_factory_instead_of_caching_result(): void + { + $calls = 0; + $fixture = $this->fixture(entryPointFactory: static function () use (&$calls): ComponentEntryPoint { + ++$calls; + + return new DefinitionFixtureEntryPoint(); + }); + + $first = $fixture->createEntryPoint(); + $second = $fixture->createEntryPoint(); + + self::assertSame(2, $calls); + self::assertNotSame($first, $second); + } + + private function fixture( + ?ComponentIdentifier $identifier = null, + ComponentType $type = ComponentType::Module, + ?ComponentGraphRelations $relations = null, + ?\Closure $entryPointFactory = null, + ?\Closure $validator = null, + ): ComponentDefinitionFixture { + return new ComponentDefinitionFixture( + $identifier ?? new ComponentIdentifier('app/testing-fixture'), + $type, + $entryPointFactory ?? static fn(): ComponentEntryPoint => new DefinitionFixtureEntryPoint(), + $relations ?? new ComponentGraphRelations(), + $validator, + ); + } +} + +final class DefinitionFixtureEntryPoint implements ComponentEntryPoint +{ + public function register(ServiceDefinitionRegistrar $registrar): void {} + + public function boot(ComponentBootContext $context): void {} + + public function ready(): void {} + + public function shutdown(): void {} +} diff --git a/packages/testing/tests/Unit/Component/ComponentEntryPointFixtureTest.php b/packages/testing/tests/Unit/Component/ComponentEntryPointFixtureTest.php new file mode 100644 index 0000000..cb4a72c --- /dev/null +++ b/packages/testing/tests/Unit/Component/ComponentEntryPointFixtureTest.php @@ -0,0 +1,178 @@ +register(new EntryPointFixtureRegistrar()); + $fixture->boot(new EntryPointFixtureBootContext()); + $fixture->ready(); + $fixture->shutdown(); + + self::addToAssertionCount(1); + } + + public function test_direct_lifecycle_invocations_are_not_once_only_guarded_by_fixture(): void + { + $registrations = 0; + $boots = 0; + $readyCalls = 0; + $shutdowns = 0; + $fixture = new ComponentEntryPointFixture( + register: static function (ServiceDefinitionRegistrar $registrar) use (&$registrations): void { + ++$registrations; + }, + boot: static function (ComponentBootContext $context) use (&$boots): void { + ++$boots; + }, + ready: static function () use (&$readyCalls): void { + ++$readyCalls; + }, + shutdown: static function () use (&$shutdowns): void { + ++$shutdowns; + }, + ); + $registrar = new EntryPointFixtureRegistrar(); + $context = new EntryPointFixtureBootContext(); + + $fixture->register($registrar); + $fixture->register($registrar); + $fixture->boot($context); + $fixture->boot($context); + $fixture->ready(); + $fixture->ready(); + $fixture->shutdown(); + $fixture->shutdown(); + + self::assertSame(2, $registrations); + self::assertSame(2, $boots); + self::assertSame(2, $readyCalls); + self::assertSame(2, $shutdowns); + } + + public function test_register_callback_is_invoked_with_exact_registrar(): void + { + $registrar = new EntryPointFixtureRegistrar(); + $calls = 0; + $forwarded = null; + $fixture = new ComponentEntryPointFixture( + register: static function (ServiceDefinitionRegistrar $callbackRegistrar) use (&$calls, &$forwarded): void { + ++$calls; + $forwarded = $callbackRegistrar; + }, + ); + + $fixture->register($registrar); + + self::assertSame(1, $calls); + self::assertSame($registrar, $forwarded); + } + + public function test_boot_callback_is_invoked_with_exact_context(): void + { + $context = new EntryPointFixtureBootContext(); + $calls = 0; + $forwarded = null; + $fixture = new ComponentEntryPointFixture( + boot: static function (ComponentBootContext $callbackContext) use (&$calls, &$forwarded): void { + ++$calls; + $forwarded = $callbackContext; + }, + ); + + $fixture->boot($context); + + self::assertSame(1, $calls); + self::assertSame($context, $forwarded); + } + + public function test_ready_callback_is_invoked(): void + { + $calls = 0; + $fixture = new ComponentEntryPointFixture( + ready: static function () use (&$calls): void { + ++$calls; + }, + ); + + $fixture->ready(); + + self::assertSame(1, $calls); + } + + public function test_shutdown_callback_is_invoked(): void + { + $calls = 0; + $fixture = new ComponentEntryPointFixture( + shutdown: static function () use (&$calls): void { + ++$calls; + }, + ); + + $fixture->shutdown(); + + self::assertSame(1, $calls); + } + + public function test_callback_exceptions_propagate_unchanged(): void + { + $failure = new RuntimeException('callback failed'); + $fixture = new ComponentEntryPointFixture( + ready: static function () use ($failure): void { + throw $failure; + }, + ); + + try { + $fixture->ready(); + self::fail('Callback failure should propagate.'); + } catch (RuntimeException $exception) { + self::assertSame($failure, $exception); + } + } +} + +final class EntryPointFixtureRegistrar implements ServiceDefinitionRegistrar +{ + public function registerApplication(string $id, callable $factory): void {} + + public function registerExecution(string $id, callable $factory): void {} + + public function registerTransient(string $id, callable $factory): void {} +} + +final class EntryPointFixtureBootContext implements ComponentBootContext +{ + public function services(): ContainerInterface + { + return new EntryPointFixtureContainer(); + } + + public function deferFailureCleanup(callable $cleanup): void {} +} + +final class EntryPointFixtureContainer implements ContainerInterface +{ + public function get(string $id): mixed + { + throw new RuntimeException('No services are available in this test container.'); + } + + public function has(string $id): bool + { + return false; + } +} diff --git a/tests/Architecture/EvolvePhp2PackageSkeletonTest.php b/tests/Architecture/EvolvePhp2PackageSkeletonTest.php index 02797b5..9bc4bf1 100644 --- a/tests/Architecture/EvolvePhp2PackageSkeletonTest.php +++ b/tests/Architecture/EvolvePhp2PackageSkeletonTest.php @@ -133,7 +133,7 @@ public function testPackageSourcesMatchAcceptedPackageSourceInventory(): void $this->assertFileDoesNotExist($this->projectPath('packages/http/src/.gitkeep')); $this->assertFileDoesNotExist($this->projectPath('packages/module/src/.gitkeep')); $this->assertFileDoesNotExist($this->projectPath('packages/plugin/src/.gitkeep')); - $this->assertFileExists($this->projectPath('packages/testing/src/.gitkeep')); + $this->assertFileDoesNotExist($this->projectPath('packages/testing/src/.gitkeep')); } public function testPackageOverviewDocumentsSkeletonBoundariesAndCompatibilityLimits(): void @@ -841,7 +841,10 @@ private function acceptedPackageSourceInventories() 'PluginDefinition.php', 'PluginDescriptor.php', ), - 'packages/testing/src' => array(), + 'packages/testing/src' => array( + 'Component/ComponentDefinitionFixture.php', + 'Component/ComponentEntryPointFixture.php', + ), ); }