From 879e0be4b7c1084bb2eeecd2d81161fe95884aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= <5175937+theofidry@users.noreply.github.com> Date: Mon, 11 Mar 2024 09:46:42 +0100 Subject: [PATCH] feat: Provide a non optimized requirement list (#1328) This is a preparatory step for #1273. The goal is to be able to provide _all_ the requirements. The idea being that one may be interested in seeing this full list for debugging purpose, rather than the strict final one where the provided extensions removed the required extensions from the calculated requirements. --- .../AppRequirementsFactory.php | 30 ++- src/RequirementChecker/Requirement.php | 29 +++ src/RequirementChecker/RequirementType.php | 1 + .../RequirementsBuilder.php | 60 +++++ tests/RequirementChecker/RequirementTest.php | 36 +++ .../RequirementsBuilderTest.php | 229 ++++++++++++------ 6 files changed, 314 insertions(+), 71 deletions(-) diff --git a/src/RequirementChecker/AppRequirementsFactory.php b/src/RequirementChecker/AppRequirementsFactory.php index db83165f2..5aede6d6e 100644 --- a/src/RequirementChecker/AppRequirementsFactory.php +++ b/src/RequirementChecker/AppRequirementsFactory.php @@ -28,11 +28,39 @@ final class AppRequirementsFactory { private const SELF_PACKAGE = null; + public function createUnfiltered( + ComposerJson $composerJson, + ComposerLock $composerLock, + CompressionAlgorithm $compressionAlgorithm, + ): Requirements { + return $this + ->createBuilder( + $composerJson, + $composerLock, + $compressionAlgorithm, + ) + ->all(); + } + public function create( ComposerJson $composerJson, ComposerLock $composerLock, CompressionAlgorithm $compressionAlgorithm, ): Requirements { + return $this + ->createBuilder( + $composerJson, + $composerLock, + $compressionAlgorithm, + ) + ->build(); + } + + private function createBuilder( + ComposerJson $composerJson, + ComposerLock $composerLock, + CompressionAlgorithm $compressionAlgorithm, + ): RequirementsBuilder { $requirementsBuilder = new RequirementsBuilder(); self::retrievePhpVersionRequirements($requirementsBuilder, $composerJson, $composerLock); @@ -40,7 +68,7 @@ public function create( self::collectComposerLockExtensionRequirements($composerLock, $requirementsBuilder); self::collectComposerJsonExtensionRequirements($composerJson, $requirementsBuilder); - return $requirementsBuilder->build(); + return $requirementsBuilder; } private static function retrievePhpVersionRequirements( diff --git a/src/RequirementChecker/Requirement.php b/src/RequirementChecker/Requirement.php index fe1d8e3ef..c5752b275 100644 --- a/src/RequirementChecker/Requirement.php +++ b/src/RequirementChecker/Requirement.php @@ -86,6 +86,35 @@ public static function forRequiredExtension(string $extension, ?string $packageN ); } + public static function forProvidedExtension(string $extension, ?string $packageName): self + { + return new self( + RequirementType::PROVIDED_EXTENSION, + $extension, + $packageName, + null === $packageName + ? sprintf( + 'This application provides the extension "%s".', + $extension, + ) + : sprintf( + 'The package "%s" provides the extension "%s".', + $packageName, + $extension, + ), + null === $packageName + ? sprintf( + 'This application does not require the extension "%s", it is provided by the application itself.', + $extension, + ) + : sprintf( + 'This application does not require the extension "%s", it is provided by the package "%s".', + $packageName, + $extension, + ), + ); + } + public static function forConflictingExtension(string $extension, ?string $packageName): self { return new self( diff --git a/src/RequirementChecker/RequirementType.php b/src/RequirementChecker/RequirementType.php index e5edb1882..89d5e642a 100644 --- a/src/RequirementChecker/RequirementType.php +++ b/src/RequirementChecker/RequirementType.php @@ -18,5 +18,6 @@ enum RequirementType: string { case PHP = 'php'; case EXTENSION = 'extension'; + case PROVIDED_EXTENSION = 'provided-extension'; case EXTENSION_CONFLICT = 'extension-conflict'; } diff --git a/src/RequirementChecker/RequirementsBuilder.php b/src/RequirementChecker/RequirementsBuilder.php index 0ce061248..d4518edc9 100644 --- a/src/RequirementChecker/RequirementsBuilder.php +++ b/src/RequirementChecker/RequirementsBuilder.php @@ -49,6 +49,40 @@ public function addConflictingExtension(Extension $extension, ?string $source): $this->conflictingExtensions[$extension->name][] = $source; } + public function all(): Requirements + { + $requirements = $this->predefinedRequirements; + + foreach ($this->getUnfilteredSortedRequiredExtensions() as $extensionName => $sources) { + foreach ($sources as $source) { + $requirements[] = Requirement::forRequiredExtension( + $extensionName, + $source, + ); + } + } + + foreach ($this->getSortedProvidedExtensions() as $extensionName => $sources) { + foreach ($sources as $source) { + $requirements[] = Requirement::forProvidedExtension( + $extensionName, + $source, + ); + } + } + + foreach ($this->getSortedConflictedExtensions() as $extensionName => $sources) { + foreach ($sources as $source) { + $requirements[] = Requirement::forConflictingExtension( + $extensionName, + $source, + ); + } + } + + return new Requirements($requirements); + } + public function build(): Requirements { $requirements = $this->predefinedRequirements; @@ -74,6 +108,32 @@ public function build(): Requirements return new Requirements($requirements); } + /** + * @return array> + */ + private function getUnfilteredSortedRequiredExtensions(): array + { + return array_map( + self::createSortedDistinctList(...), + self::sortByExtensionName( + $this->requiredExtensions, + ), + ); + } + + /** + * @return array> + */ + private function getSortedProvidedExtensions(): array + { + return array_map( + self::createSortedDistinctList(...), + self::sortByExtensionName( + $this->providedExtensions, + ), + ); + } + /** * @return array> */ diff --git a/tests/RequirementChecker/RequirementTest.php b/tests/RequirementChecker/RequirementTest.php index f72403734..ef30569de 100644 --- a/tests/RequirementChecker/RequirementTest.php +++ b/tests/RequirementChecker/RequirementTest.php @@ -95,6 +95,42 @@ public function test_it_can_be_created_for_an_extension_constraint_for_a_package self::assertItCanBeCreatedFromItsArrayForm($requirement, $actual); } + public function test_it_can_be_created_for_a_provided_extension_constraint(): void + { + $requirement = Requirement::forProvidedExtension('mbstring', null); + + $expected = [ + 'type' => 'provided-extension', + 'condition' => 'mbstring', + 'source' => null, + 'message' => 'This application provides the extension "mbstring".', + 'helpMessage' => 'This application does not require the extension "mbstring", it is provided by the application itself.', + ]; + + $actual = $requirement->toArray(); + + self::assertSame($expected, $actual); + self::assertItCanBeCreatedFromItsArrayForm($requirement, $actual); + } + + public function test_it_can_be_created_for_a_provided_extension_constraint_for_a_package(): void + { + $requirement = Requirement::forProvidedExtension('mbstring', 'box/test'); + + $expected = [ + 'type' => 'provided-extension', + 'condition' => 'mbstring', + 'source' => 'box/test', + 'message' => 'The package "box/test" provides the extension "mbstring".', + 'helpMessage' => 'This application does not require the extension "box/test", it is provided by the package "mbstring".', + ]; + + $actual = $requirement->toArray(); + + self::assertSame($expected, $actual); + self::assertItCanBeCreatedFromItsArrayForm($requirement, $actual); + } + public function test_it_can_be_created_for_a_conflicting_extension_constraint(): void { $requirement = Requirement::forConflictingExtension('mbstring', null); diff --git a/tests/RequirementChecker/RequirementsBuilderTest.php b/tests/RequirementChecker/RequirementsBuilderTest.php index 028ffe97a..559c0ae71 100644 --- a/tests/RequirementChecker/RequirementsBuilderTest.php +++ b/tests/RequirementChecker/RequirementsBuilderTest.php @@ -25,13 +25,19 @@ #[CoversClass(RequirementsBuilder::class)] final class RequirementsBuilderTest extends TestCase { - public function test_it_can_build_requirements_from_an_empty_list(): void + private RequirementsBuilder $builder; + + protected function setUp(): void { - $requirements = (new RequirementsBuilder())->build(); + $this->builder = new RequirementsBuilder(); + } + public function test_it_can_build_requirements_from_an_empty_list(): void + { $expected = new Requirements([]); - self::assertEquals($expected, $requirements); + $this->assertBuiltRequirementsEquals($expected); + $this->assertAllRequirementsEquals($expected); } public function test_it_can_build_requirements_from_predefined_requirements(): void @@ -42,39 +48,36 @@ public function test_it_can_build_requirements_from_predefined_requirements(): v Requirement::forConflictingExtension('http', null), ]; - $builder = new RequirementsBuilder(); - foreach ($predefinedRequirements as $predefinedRequirement) { - $builder->addRequirement($predefinedRequirement); + $this->builder->addRequirement($predefinedRequirement); } $expected = new Requirements($predefinedRequirements); - $actual = $builder->build(); - self::assertEquals($expected, $actual); + $this->assertBuiltRequirementsEquals($expected); + $this->assertAllRequirementsEquals($expected); } public function test_it_can_build_requirements_from_required_extensions(): void { - $builder = new RequirementsBuilder(); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('http'), 'package1', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('http'), 'package2', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('phar'), 'package1', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('openssl'), 'package3', ); // Duplicate - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('openssl'), 'package3', ); @@ -86,51 +89,51 @@ public function test_it_can_build_requirements_from_required_extensions(): void Requirement::forRequiredExtension('phar', 'package1'), ]); - $actual = $builder->build(); - - self::assertEquals($expected, $actual); + $this->assertBuiltRequirementsEquals($expected); + $this->assertAllRequirementsEquals($expected); } public function test_it_can_build_requirements_from_provided_extensions(): void { - $builder = new RequirementsBuilder(); - $builder->addProvidedExtension( + $this->builder->addProvidedExtension( new Extension('http'), 'package1', ); - $builder->addProvidedExtension( + $this->builder->addProvidedExtension( new Extension('http'), 'package2', ); - $expected = new Requirements([]); - - $actual = $builder->build(); + $expectedBuiltRequirements = new Requirements([]); + $expectedAllRequirements = new Requirements([ + Requirement::forProvidedExtension('http', 'package1'), + Requirement::forProvidedExtension('http', 'package2'), + ]); - self::assertEquals($expected, $actual); + $this->assertBuiltRequirementsEquals($expectedBuiltRequirements); + $this->assertAllRequirementsEquals($expectedAllRequirements); } public function test_it_can_build_requirements_from_conflicting_extensions(): void { - $builder = new RequirementsBuilder(); - $builder->addConflictingExtension( + $this->builder->addConflictingExtension( new Extension('http'), 'package1', ); - $builder->addConflictingExtension( + $this->builder->addConflictingExtension( new Extension('http'), 'package2', ); - $builder->addConflictingExtension( + $this->builder->addConflictingExtension( new Extension('phar'), 'package1', ); - $builder->addConflictingExtension( + $this->builder->addConflictingExtension( new Extension('openssl'), 'package3', ); // Duplicate - $builder->addConflictingExtension( + $this->builder->addConflictingExtension( new Extension('openssl'), 'package3', ); @@ -142,88 +145,96 @@ public function test_it_can_build_requirements_from_conflicting_extensions(): vo Requirement::forConflictingExtension('phar', 'package1'), ]); - $actual = $builder->build(); - - self::assertEquals($expected, $actual); + $this->assertBuiltRequirementsEquals($expected); + $this->assertAllRequirementsEquals($expected); } public function test_it_removes_extension_requirements_if_they_are_provided(): void { - $builder = new RequirementsBuilder(); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('http'), 'package1', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('http'), 'package2', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('phar'), 'package1', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('openssl'), 'package3', ); - $builder->addProvidedExtension( + $this->builder->addProvidedExtension( new Extension('http'), 'package3', ); - $expected = new Requirements([ + $expectedBuiltRequirements = new Requirements([ Requirement::forRequiredExtension('openssl', 'package3'), Requirement::forRequiredExtension('phar', 'package1'), ]); + $expectedAllRequirements = new Requirements([ + Requirement::forRequiredExtension('http', 'package1'), + Requirement::forRequiredExtension('http', 'package2'), + Requirement::forRequiredExtension('openssl', 'package3'), + Requirement::forRequiredExtension('phar', 'package1'), + Requirement::forProvidedExtension('http', 'package3'), + ]); - $actual = $builder->build(); - - self::assertEquals($expected, $actual); + $this->assertBuiltRequirementsEquals($expectedBuiltRequirements); + $this->assertAllRequirementsEquals($expectedAllRequirements); } public function test_it_does_not_remove_extension_conflicts_if_they_are_provided(): void { - $builder = new RequirementsBuilder(); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('http'), 'package1', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('http'), 'package2', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('phar'), 'package1', ); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('openssl'), 'package3', ); - $builder->addProvidedExtension( + $this->builder->addProvidedExtension( new Extension('http'), 'package3', ); - $expected = new Requirements([ + $expectedBuiltRequirements = new Requirements([ Requirement::forRequiredExtension('openssl', 'package3'), Requirement::forRequiredExtension('phar', 'package1'), ]); + $expectedAllRequirements = new Requirements([ + Requirement::forRequiredExtension('http', 'package1'), + Requirement::forRequiredExtension('http', 'package2'), + Requirement::forRequiredExtension('openssl', 'package3'), + Requirement::forRequiredExtension('phar', 'package1'), + Requirement::forProvidedExtension('http', 'package3'), + ]); - $actual = $builder->build(); - - self::assertEquals($expected, $actual); + $this->assertBuiltRequirementsEquals($expectedBuiltRequirements); + $this->assertAllRequirementsEquals($expectedAllRequirements); } public function test_it_can_have_an_extension_that_is_required_and_conflicting_at_the_same_time(): void { // This scenario does not really make sense but ensuring this does not happen is Composer's job not Box. - $builder = new RequirementsBuilder(); - $builder->addRequiredExtension( + $this->builder->addRequiredExtension( new Extension('http'), 'package1', ); - $builder->addConflictingExtension( + $this->builder->addConflictingExtension( new Extension('http'), 'package2', ); @@ -233,9 +244,11 @@ public function test_it_can_have_an_extension_that_is_required_and_conflicting_a Requirement::forConflictingExtension('http', 'package2'), ]); - $actual = $builder->build(); + $builtRequirements = $this->builder->build(); + $allRequirements = $this->builder->all(); - self::assertEquals($expected, $actual); + self::assertEquals($expected, $builtRequirements); + self::assertEquals($expected, $allRequirements); } // TODO: this could be solved @@ -243,16 +256,15 @@ public function test_it_does_not_remove_predefined_requirements_even_if_they_are { $predefinedRequirement = Requirement::forRequiredExtension('http', null); - $builder = new RequirementsBuilder(); - $builder->addRequirement($predefinedRequirement); - $builder->addProvidedExtension( + $this->builder->addRequirement($predefinedRequirement); + $this->builder->addProvidedExtension( new Extension('http'), 'package3', ); $expected = new Requirements([$predefinedRequirement]); - $actual = $builder->build(); + $actual = $this->builder->build(); self::assertEquals($expected, $actual); } @@ -262,25 +274,28 @@ public function test_it_ensures_the_requirements_built_are_consistent( array $predefinedRequirements, array $requiredExtensionSourcePairs, array $conflictingExtensionSourcePairs, - Requirements $expected, + array $providedExtensionSourcePairs, + Requirements $expectedBuiltRequirements, + Requirements $expectedAllRequirements, ): void { - $builder = new RequirementsBuilder(); - foreach ($predefinedRequirements as $predefinedRequirement) { - $builder->addRequirement($predefinedRequirement); + $this->builder->addRequirement($predefinedRequirement); } foreach ($requiredExtensionSourcePairs as [$requiredExtension, $source]) { - $builder->addRequiredExtension($requiredExtension, $source); + $this->builder->addRequiredExtension($requiredExtension, $source); } foreach ($conflictingExtensionSourcePairs as [$conflictingExtension, $source]) { - $builder->addConflictingExtension($conflictingExtension, $source); + $this->builder->addConflictingExtension($conflictingExtension, $source); } - $actual = $builder->build(); + foreach ($providedExtensionSourcePairs as [$conflictingExtension, $source]) { + $this->builder->addProvidedExtension($conflictingExtension, $source); + } - self::assertEquals($expected, $actual); + $this->assertBuiltRequirementsEquals($expectedBuiltRequirements); + $this->assertAllRequirementsEquals($expectedAllRequirements); } public static function requirementsProvider(): iterable @@ -297,6 +312,12 @@ public static function requirementsProvider(): iterable ], [], [], + [], + new Requirements([ + $predefinedRequirementZ, + $predefinedRequirementNull, + $predefinedRequirementA, + ]), new Requirements([ $predefinedRequirementZ, $predefinedRequirementNull, @@ -312,6 +333,12 @@ public static function requirementsProvider(): iterable [new Extension('noop'), 'A'], ], [], + [], + new Requirements([ + Requirement::forRequiredExtension('noop', null), + Requirement::forRequiredExtension('noop', 'A'), + Requirement::forRequiredExtension('noop', 'Z'), + ]), new Requirements([ Requirement::forRequiredExtension('noop', null), Requirement::forRequiredExtension('noop', 'A'), @@ -326,6 +353,11 @@ public static function requirementsProvider(): iterable [new Extension('a-ext'), null], ], [], + [], + new Requirements([ + Requirement::forRequiredExtension('a-ext', null), + Requirement::forRequiredExtension('z-ext', null), + ]), new Requirements([ Requirement::forRequiredExtension('a-ext', null), Requirement::forRequiredExtension('z-ext', null), @@ -340,6 +372,12 @@ public static function requirementsProvider(): iterable [new Extension('noop'), null], [new Extension('noop'), 'A'], ], + [], + new Requirements([ + Requirement::forConflictingExtension('noop', null), + Requirement::forConflictingExtension('noop', 'A'), + Requirement::forConflictingExtension('noop', 'Z'), + ]), new Requirements([ Requirement::forConflictingExtension('noop', null), Requirement::forConflictingExtension('noop', 'A'), @@ -354,10 +392,61 @@ public static function requirementsProvider(): iterable [new Extension('z-ext'), null], [new Extension('a-ext'), null], ], + [], + new Requirements([ + Requirement::forConflictingExtension('a-ext', null), + Requirement::forConflictingExtension('z-ext', null), + ]), new Requirements([ Requirement::forConflictingExtension('a-ext', null), Requirement::forConflictingExtension('z-ext', null), ]), ]; + + yield 'provided extension sources' => [ + [], + [], + [], + [ + [new Extension('noop'), 'Z'], + [new Extension('noop'), null], + [new Extension('noop'), 'A'], + ], + new Requirements([]), + new Requirements([ + Requirement::forProvidedExtension('noop', null), + Requirement::forProvidedExtension('noop', 'A'), + Requirement::forProvidedExtension('noop', 'Z'), + ]), + ]; + + yield 'provided extensions' => [ + [], + [], + [], + [ + [new Extension('z-ext'), null], + [new Extension('a-ext'), null], + ], + new Requirements([]), + new Requirements([ + Requirement::forProvidedExtension('a-ext', null), + Requirement::forProvidedExtension('z-ext', null), + ]), + ]; + } + + private function assertBuiltRequirementsEquals(Requirements $expected): void + { + $actual = $this->builder->build(); + + self::assertEquals($expected, $actual); + } + + private function assertAllRequirementsEquals(Requirements $expected): void + { + $actual = $this->builder->all(); + + self::assertEquals($expected, $actual); } }