}> */ private array $packages = []; protected function setUp(): void { parent::setUp(); $this->projectRoot = TEST_PROJECT_ROOT; $this->packages = $this->loadPackages(); } public function testLocalPluginDependencyGraphIsAcyclic(): void { $graph = $this->localGraph(); $state = []; $stack = []; $cycle = []; $visit = function (string $package) use (&$visit, &$graph, &$state, &$stack, &$cycle): void { if ($cycle !== []) { return; } $state[$package] = 1; $stack[] = $package; foreach ($graph[$package] ?? [] as $dependency) { $depState = $state[$dependency] ?? 0; if ($depState === 0) { $visit($dependency); if ($cycle !== []) { return; } continue; } if ($depState === 1) { $offset = array_search($dependency, $stack, true); $cycle = array_slice($stack, $offset === false ? 0 : $offset); $cycle[] = $dependency; return; } } array_pop($stack); $state[$package] = 2; }; foreach (array_keys($graph) as $package) { if (($state[$package] ?? 0) === 0) { $visit($package); } } $this->assertSame([], $cycle, 'Local composer dependency cycle detected: ' . implode(' -> ', $cycle)); } public function testBasePackagesStayAtTheBottomOfDependencyGraph(): void { $this->assertSame([], $this->localRequires('zoujingli/think-library')); $this->assertSame([ 'zoujingli/think-library', 'zoujingli/think-plugs-static', 'zoujingli/think-plugs-worker', ], $this->localRequires('zoujingli/think-plugs-system')); $this->assertSame(['zoujingli/think-library'], $this->localRequires('zoujingli/think-plugs-worker')); } public function testSystemDoesNotReintroduceLegacyStoragePackage(): void { $system = $this->localRequires('zoujingli/think-plugs-system'); $this->assertNotContains('zoujingli/think-plugs-storage', $system); } public function testHelperPackageStaysMergedIntoSystem(): void { $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-helper')); $this->assertFileExists($this->path('plugin/think-plugs-system/src/helper/Service.php')); $this->assertFileExists($this->path('plugin/think-plugs-system/src/helper/command/project/PublishCommand.php')); $legacyHelperNamespace = 'plugin' . '\\helper\\'; $systemComposer = $this->jsonFile($this->path('plugin/think-plugs-system/composer.json')); $systemPsr4 = is_array($systemComposer['autoload']['psr-4'] ?? null) ? $systemComposer['autoload']['psr-4'] : []; $systemServices = is_array($systemComposer['extra']['think']['services'] ?? null) ? $systemComposer['extra']['think']['services'] : []; $this->assertArrayNotHasKey($legacyHelperNamespace, $systemPsr4); $this->assertContains('plugin\system\helper\Service', $systemServices); $this->assertNotContains($legacyHelperNamespace . 'Service', $systemServices); $violations = []; foreach ($this->composerFiles() as $file) { $json = $this->jsonFile($file); if (str_ends_with($file, '/composer.lock')) { $packages = array_merge( is_array($json['packages'] ?? null) ? $json['packages'] : [], is_array($json['packages-dev'] ?? null) ? $json['packages-dev'] : [] ); foreach ($packages as $package) { if (!is_array($package)) { continue; } if (($package['name'] ?? null) === 'zoujingli/think-plugs-helper') { $violations[] = [$file, 'package']; } foreach (['require', 'require-dev'] as $section) { if (isset($package[$section]['zoujingli/think-plugs-helper'])) { $violations[] = [$file, $section, $package['name'] ?? 'unknown']; } } } continue; } foreach (['require', 'require-dev'] as $section) { if (isset($json[$section]['zoujingli/think-plugs-helper'])) { $violations[] = [$file, $section]; } } } $this->assertSame([], $violations, 'Legacy helper package dependencies found: ' . json_encode($violations, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); } public function testLegacyViewPackagesStayRemoved(): void { $legacyPackages = [ 'zoujingli/think-plugs-view' => 'plugin/think-plugs-view', 'zoujingli/think-plugs-system-view' => 'plugin/think-plugs-system-view', 'zoujingli/think-plugs-wechat-client-view' => 'plugin/think-plugs-wechat-client-view', 'zoujingli/think-plugs-wechat-service-view' => 'plugin/think-plugs-wechat-service-view', ]; foreach ($legacyPackages as $package => $directory) { $this->assertDirectoryDoesNotExist($this->path($directory), $package . ' directory should stay removed'); } $violations = []; foreach ($this->composerFiles() as $file) { $content = file_get_contents($file) ?: ''; foreach (array_keys($legacyPackages) as $package) { if (strpos($content, $package) !== false) { $violations[] = [$package, $file]; } } } $this->assertSame([], $violations, 'Legacy view package references found: ' . json_encode($violations, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); } public function testEveryLocalDependencyPointsToAnExistingWorkspacePackage(): void { $known = array_keys($this->packages); $missing = []; foreach ($this->packages as $name => $package) { foreach ($this->localRequires($name) as $dependency) { if (!in_array($dependency, $known, true)) { $missing[] = [$name, $dependency]; } } } $this->assertSame([], $missing, 'Unknown local package dependencies found: ' . json_encode($missing, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); } /** * @return array}> */ private function loadPackages(): array { $items = []; foreach (glob($this->path('plugin/*/composer.json')) ?: [] as $file) { $json = json_decode(file_get_contents($file) ?: '', true); if (!is_array($json) || empty($json['name'])) { continue; } $items[strval($json['name'])] = [ 'name' => strval($json['name']), 'path' => $file, 'require' => is_array($json['require'] ?? null) ? $json['require'] : [], ]; } ksort($items); return $items; } /** * @return array> */ private function localGraph(): array { $graph = []; foreach (array_keys($this->packages) as $name) { $graph[$name] = $this->localRequires($name); } return $graph; } /** * @return list */ private function localRequires(string $package): array { $requires = array_keys($this->packages[$package]['require'] ?? []); $locals = array_values(array_filter($requires, fn (string $name): bool => isset($this->packages[$name]))); sort($locals); return $locals; } /** * @return list */ private function composerFiles(): array { $files = []; foreach (['composer.json', 'composer.lock'] as $name) { $file = $this->path($name); if (is_file($file)) { $files[] = str_replace('\\', '/', $file); } } foreach (glob($this->path('plugin/*/composer.json')) ?: [] as $file) { $files[] = str_replace('\\', '/', $file); } sort($files); return $files; } /** * @return array */ private function jsonFile(string $file): array { $json = json_decode(file_get_contents($file) ?: '', true); return is_array($json) ? $json : []; } private function path(string $relative): string { return $this->projectRoot . '/' . ltrim($relative, '/'); } }