From 32d2376c9d5c551314a412a230a5c7e556710bdb Mon Sep 17 00:00:00 2001 From: Richard Klees Date: Wed, 22 Nov 2023 12:38:37 +0100 Subject: [PATCH] Component/Dependencies: Find Cycles in Dependency Tree --- .../ILIAS/Component/src/Dependencies/Out.php | 5 ++ .../Component/src/Dependencies/Resolver.php | 50 +++++++++++++++++++ .../tests/Dependencies/ResolverTest.php | 47 +++++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/components/ILIAS/Component/src/Dependencies/Out.php b/components/ILIAS/Component/src/Dependencies/Out.php index 6c78296b258c..c70c2c4c1c6a 100644 --- a/components/ILIAS/Component/src/Dependencies/Out.php +++ b/components/ILIAS/Component/src/Dependencies/Out.php @@ -82,6 +82,11 @@ public function addDependency(In $in): void $this->dependencies[(string) $in] = $in; } + public function getDependencies(): array + { + return $this->dependencies; + } + public function addResolves(In $in): void { $this->resolves[] = $in; diff --git a/components/ILIAS/Component/src/Dependencies/Resolver.php b/components/ILIAS/Component/src/Dependencies/Resolver.php index 66b95e5acab9..9a38a4f8c362 100644 --- a/components/ILIAS/Component/src/Dependencies/Resolver.php +++ b/components/ILIAS/Component/src/Dependencies/Resolver.php @@ -68,6 +68,23 @@ public function resolveDependencies(array $disambiguation, OfComponent ...$compo } } + $cycles = iterator_to_array($this->findCycles(...$components)); + if (!empty($cycles)) { + throw new \LogicException( + "Detected Cycles in Dependency Tree: " . + join("\n", array_map( + fn($cycle) => join( + " <- ", + array_map( + fn($v) => "{$v[0]->getComponentName()} ({$v[1]})", + $cycle + ) + ), + $cycles + )) + ); + } + return $components; } @@ -157,4 +174,37 @@ protected function disambiguate(OfComponent $component, array &$disambiguation, } return null; } + + /** + * @var Generator> + */ + protected function findCycles(OfComponent ...$components): \Generator + { + foreach ($components as $component) { + foreach ($component->getInDependencies() as $in) { + foreach ($this->findCyclesWith([], $component, $in) as $cycle) { + yield $cycle; + } + } + } + } + + protected function findCyclesWith(array $visited, OfComponent $component, In $in): \Generator + { + if (!empty($visited) && $visited[0][0] === $component && $visited[0][1] == $in) { + yield $visited; + return; + } + + array_push($visited, [$component, $in]); + foreach ($in->getResolvedBy() as $out) { + $other = $out->getComponent(); + array_push($visited, [$component, $out]); + foreach ($out->getDependencies() as $next) { + yield from $this->findCyclesWith($visited, $out->getComponent(), $next); + } + array_pop($visited); + } + array_pop($visited); + } } diff --git a/components/ILIAS/Component/tests/Dependencies/ResolverTest.php b/components/ILIAS/Component/tests/Dependencies/ResolverTest.php index b3be9987afe9..be44a8a658ad 100644 --- a/components/ILIAS/Component/tests/Dependencies/ResolverTest.php +++ b/components/ILIAS/Component/tests/Dependencies/ResolverTest.php @@ -270,4 +270,51 @@ public function testUseDisambiguateDuplicateGeneric(): void $this->assertEquals([$c1, $c2, $c3], $result); } + + public function testFindSimpleCycle(): void + { + $this->expectException(\LogicException::class); + + $component = $this->createMock(Component::class); + + $name = TestInterface::class; + $name2 = TestInterface2::class; + + $pull = new D\In(D\InType::PULL, $name); + $provide = new D\Out(D\OutType::PROVIDE, $name2, "Some\\Class", [$pull], []); + $c1 = new D\OfComponent($component, $pull, $provide); + + $pull = new D\In(D\InType::PULL, $name2); + $provide = new D\Out(D\OutType::PROVIDE, $name, "Some\\OtherClass", [$pull], []); + $c2 = new D\OfComponent($component, $pull, $provide); + + + $result = $this->resolver->resolveDependencies([], $c1, $c2); + } + + public function testFindLongerCycle(): void + { + $this->expectException(\LogicException::class); + + $component = $this->createMock(Component::class); + + $name = TestInterface::class; + $name2 = TestInterface2::class; + $name3 = TestInterface3::class; + + $pull = new D\In(D\InType::PULL, $name); + $provide = new D\Out(D\OutType::PROVIDE, $name2, "Some\\Class", [$pull], []); + $c1 = new D\OfComponent($component, $pull, $provide); + + $pull = new D\In(D\InType::PULL, $name2); + $provide = new D\Out(D\OutType::PROVIDE, $name3, "Some\\OtherClass", [$pull], []); + $c2 = new D\OfComponent($component, $pull, $provide); + + $pull = new D\In(D\InType::PULL, $name3); + $provide = new D\Out(D\OutType::PROVIDE, $name, "Some\\OtherOtherClass", [$pull], []); + $c3 = new D\OfComponent($component, $pull, $provide); + + + $result = $this->resolver->resolveDependencies([], $c1, $c2, $c3); + } }