Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions src/Command/Api/ApiCommandHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -532,10 +532,11 @@ private function generateApiListCommands(array $apiCommands, string $commandPref
continue;
}
$namespace = $commandNameParts[1];
if (!array_key_exists($namespace, $apiListCommands)) {
$name = $commandPrefix . ':' . $namespace;
$hasVisibleCommand = $this->namespaceHasVisibleCommand($apiCommands, $namespace);
if (!array_key_exists($name, $apiListCommands) && $hasVisibleCommand) {
/** @var \Acquia\Cli\Command\Acsf\AcsfListCommand|\Acquia\Cli\Command\Api\ApiListCommand $command */
$command = $commandFactory->createListCommand();
$name = $commandPrefix . ':' . $namespace;
$command->setName($name);
$command->setNamespace($name);
$command->setAliases([]);
Expand All @@ -546,6 +547,42 @@ private function generateApiListCommands(array $apiCommands, string $commandPref
return $apiListCommands;
}

/**
* Whether any API command in the given namespace is visible (not hidden).
*
* List commands (api:{namespace}) are only registered when at least one sub-command
* under that namespace exists and is visible. If every sub-command is hidden
* (deprecated/pre-release), the namespace list is omitted.
*
* @param ApiBaseCommand[] $apiCommands
*/
private function namespaceHasVisibleCommand(array $apiCommands, string $namespace): bool
{
$commandsInNamespace = [];
foreach ($apiCommands as $apiCommand) {
$commandNameParts = explode(':', $apiCommand->getName());
if (count($commandNameParts) < 3) {
continue;
}
if ($commandNameParts[1] !== $namespace) {
continue;
}
$commandsInNamespace[] = $apiCommand;
}

if ($commandsInNamespace === []) {
return false;
}

foreach ($commandsInNamespace as $command) {
if (!$command->isHidden()) {
return true;
}
}

return false;
}

/**
* @param array<mixed> $requestBody
* @return array<mixed>
Expand Down
8 changes: 8 additions & 0 deletions src/Command/CommandBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ public function appendHelp(string $helpText): void
$this->setHelp($helpText);
}

/**
* Helper text for IDE-only commands.
*/
public static function getIdeHelperText(): string
{
return 'This command will only work in an IDE terminal.';
}

protected static function getUuidRegexConstraint(): Regex
{
return new Regex([
Expand Down
2 changes: 2 additions & 0 deletions src/Command/Ide/IdePhpVersionCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Acquia\Cli\Command\Ide;

use Acquia\Cli\Command\CommandBase;
use Acquia\Cli\Exception\AcquiaCliException;
use Acquia\DrupalEnvironmentDetector\AcquiaDrupalEnvironmentDetector;
use Symfony\Component\Console\Attribute\AsCommand;
Expand All @@ -22,6 +23,7 @@ protected function configure(): void
$this
->addArgument('version', InputArgument::REQUIRED, 'The PHP version')
->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
$this->appendHelp(CommandBase::getIdeHelperText());
}

protected function execute(InputInterface $input, OutputInterface $output): int
Expand Down
2 changes: 2 additions & 0 deletions src/Command/Ide/IdeServiceRestartCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Acquia\Cli\Command\Ide;

use Acquia\Cli\Command\CommandBase;
use Acquia\DrupalEnvironmentDetector\AcquiaDrupalEnvironmentDetector;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
Expand All @@ -25,6 +26,7 @@ protected function configure(): void
->addUsage('apache')
->addUsage('mysql')
->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
$this->appendHelp(CommandBase::getIdeHelperText());
}

protected function execute(InputInterface $input, OutputInterface $output): int
Expand Down
2 changes: 2 additions & 0 deletions src/Command/Ide/IdeServiceStartCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Acquia\Cli\Command\Ide;

use Acquia\Cli\Command\CommandBase;
use Acquia\DrupalEnvironmentDetector\AcquiaDrupalEnvironmentDetector;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
Expand All @@ -25,6 +26,7 @@ protected function configure(): void
->addUsage('apache')
->addUsage('mysql')
->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
$this->appendHelp(CommandBase::getIdeHelperText());
}

protected function execute(InputInterface $input, OutputInterface $output): int
Expand Down
2 changes: 2 additions & 0 deletions src/Command/Ide/IdeServiceStopCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Acquia\Cli\Command\Ide;

use Acquia\Cli\Command\CommandBase;
use Acquia\DrupalEnvironmentDetector\AcquiaDrupalEnvironmentDetector;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
Expand All @@ -25,6 +26,7 @@ protected function configure(): void
->addUsage('apache')
->addUsage('mysql')
->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
$this->appendHelp(CommandBase::getIdeHelperText());
}

protected function execute(InputInterface $input, OutputInterface $output): int
Expand Down
1 change: 1 addition & 0 deletions src/Command/Ide/IdeShareCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ protected function configure(): void
$this
->addOption('regenerate', '', InputOption::VALUE_NONE, 'regenerate the share code')
->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
$this->appendHelp(CommandBase::getIdeHelperText());
}

protected function execute(InputInterface $input, OutputInterface $output): int
Expand Down
2 changes: 2 additions & 0 deletions src/Command/Ide/IdeXdebugToggleCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Acquia\Cli\Command\Ide;

use Acquia\Cli\Command\CommandBase;
use Acquia\Cli\Exception\AcquiaCliException;
use Acquia\DrupalEnvironmentDetector\AcquiaDrupalEnvironmentDetector;
use Symfony\Component\Console\Attribute\AsCommand;
Expand All @@ -20,6 +21,7 @@ protected function configure(): void
{
$this
->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv());
$this->appendHelp(CommandBase::getIdeHelperText());
}

protected function execute(InputInterface $input, OutputInterface $output): int
Expand Down
1 change: 1 addition & 0 deletions src/Command/Ide/Wizard/IdeWizardCreateSshKeyCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ protected function configure(): void
{
$this
->setHidden(!CommandBase::isAcquiaCloudIde());
$this->appendHelp(CommandBase::getIdeHelperText());
}

protected function execute(InputInterface $input, OutputInterface $output): int
Expand Down
1 change: 1 addition & 0 deletions src/Command/Ide/Wizard/IdeWizardDeleteSshKeyCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ protected function configure(): void
{
$this
->setHidden(!CommandBase::isAcquiaCloudIde());
$this->appendHelp(CommandBase::getIdeHelperText());
}

protected function execute(InputInterface $input, OutputInterface $output): int
Expand Down
14 changes: 13 additions & 1 deletion src/Command/Self/MakeDocsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@
#[AsCommand(name: 'self:make-docs', description: 'Generate documentation for all ACLI commands', hidden: true)]
final class MakeDocsCommand extends CommandBase
{
/** Commands excluded from public docs (IDE-only; hidden when not in AH IDE per AcquiaDrupalEnvironmentDetector::isAhIdeEnv()). */
private const DOCS_INCLUDED_COMMANDS = [
'ide:php-version',
'ide:service-restart',
'ide:service-start',
'ide:service-stop',
'ide:share',
'ide:wizard:ssh-key:create-upload',
'ide:wizard:ssh-key:delete',
'ide:xdebug-toggle',
];

protected function configure(): void
{
$this->addOption('format', 'f', InputOption::VALUE_OPTIONAL, 'The format to describe the docs in.', 'rst');
Expand All @@ -43,7 +55,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$commands = json_decode($buffer->fetch(), true);
$index = [];
foreach ($commands['commands'] as $command) {
if ($command['definition']['hidden'] ?? false) {
if (array_key_exists('hidden', $command) && $command['hidden'] && !in_array($command['name'], self::DOCS_INCLUDED_COMMANDS, true)) {
continue;
}
$filename = $command['name'] . '.json';
Expand Down
112 changes: 112 additions & 0 deletions tests/phpunit/src/Commands/Api/ApiCommandHelperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

declare(strict_types=1);

namespace Acquia\Cli\Tests\Commands\Api;

use Acquia\Cli\Command\Api\ApiCommandHelper;
use Acquia\Cli\Command\Api\ApiListCommand;
use Acquia\Cli\Command\CommandBase;
use Acquia\Cli\Tests\CommandTestBase;
use ReflectionMethod;
use Symfony\Component\Console\Command\Command;

/**
* Tests for ApiCommandHelper::generateApiListCommands (via reflection).
* Kills mutations in the namespace visibility and list-command creation logic.
*/
class ApiCommandHelperTest extends CommandTestBase
{
protected function createCommand(): CommandBase
{
return $this->injectCommand(ApiListCommand::class);
}

/**
* Create a mock command with the given name and hidden state.
*/
private function createMockApiCommand(string $name, bool $hidden): Command
{
$cmd = new Command($name);
$cmd->setName($name);
$cmd->setHidden($hidden);
return $cmd;
}

/**
* Call private generateApiListCommands via reflection.
*
* @param Command[] $apiCommands
* @return \Acquia\Cli\Command\Api\ApiListCommandBase[]
*/
private function generateApiListCommands(array $apiCommands, string $commandPrefix = 'api'): array
{
$helper = new ApiCommandHelper($this->logger);
$ref = new ReflectionMethod(ApiCommandHelper::class, 'generateApiListCommands');
return $ref->invoke($helper, $apiCommands, $commandPrefix, $this->getCommandFactory());
}

public function testNamespaceWithVisibleCommandGetsListCommand(): void
{
$apiCommands = [
$this->createMockApiCommand('api:foo:list', true),
$this->createMockApiCommand('api:foo:create', false),
];
$listCommands = $this->generateApiListCommands($apiCommands);
$this->assertArrayHasKey('api:foo', $listCommands);
$this->assertSame('api:foo', $listCommands['api:foo']->getName());
}

/**
* Kill LogicalAndNegation: condition must be (in namespace AND not hidden), not its negation.
* Ensures we only set hasVisibleCommand when we find a command that matches both.
*/
public function testNamespaceWithOneVisibleAndOneHiddenGetsListCommand(): void
{
$apiCommands = [
$this->createMockApiCommand('api:bar:list', false),
$this->createMockApiCommand('api:bar:create', true),
];
$listCommands = $this->generateApiListCommands($apiCommands);
$this->assertArrayHasKey('api:bar', $listCommands);
}

public function testOnlyOneListCommandPerVisibleNamespace(): void
{
$apiCommands = [
$this->createMockApiCommand('api:accounts:list', false),
$this->createMockApiCommand('api:accounts:create', false),
];
$listCommands = $this->generateApiListCommands($apiCommands);
$this->assertCount(1, $listCommands);
$this->assertArrayHasKey('api:accounts', $listCommands);
}

/**
* namespaceHasVisibleCommand must scan the full command list (continue on mismatch).
* If continue were replaced with break, a visible command after another namespace would be missed.
*/
public function testNamespaceVisibleCommandAfterOtherNamespaceStillGetsListCommand(): void
{
$apiCommands = [
$this->createMockApiCommand('api:mix:first', true),
$this->createMockApiCommand('api:other:list', false),
$this->createMockApiCommand('api:mix:second', false),
];
$listCommands = $this->generateApiListCommands($apiCommands);
$this->assertArrayHasKey('api:mix', $listCommands, 'mix has a visible command after other namespace; break would skip it.');
}

/**
* When every sub-command under a namespace is hidden, omit the namespace list command.
*/
public function testNamespaceWithAllHiddenCommandsDoesNotGetListCommand(): void
{
$apiCommands = [
$this->createMockApiCommand('api:baz:list', true),
$this->createMockApiCommand('api:baz:create', true),
];
$listCommands = $this->generateApiListCommands($apiCommands);
$this->assertArrayNotHasKey('api:baz', $listCommands);
}
}
48 changes: 48 additions & 0 deletions tests/phpunit/src/Commands/CommandBaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@
use Acquia\Cli\Command\App\LinkCommand;
use Acquia\Cli\Command\CommandBase;
use Acquia\Cli\Command\Ide\IdeListCommand;
use Acquia\Cli\Command\Ide\IdePhpVersionCommand;
use Acquia\Cli\Command\Ide\IdeServiceRestartCommand;
use Acquia\Cli\Command\Ide\IdeServiceStartCommand;
use Acquia\Cli\Command\Ide\IdeServiceStopCommand;
use Acquia\Cli\Command\Ide\IdeShareCommand;
use Acquia\Cli\Command\Ide\IdeXdebugToggleCommand;
use Acquia\Cli\Command\Ide\Wizard\IdeWizardCreateSshKeyCommand;
use Acquia\Cli\Command\Ide\Wizard\IdeWizardDeleteSshKeyCommand;
use Acquia\Cli\Exception\AcquiaCliException;
use Acquia\Cli\Tests\Commands\Ide\IdeHelper;
use Acquia\Cli\Tests\CommandTestBase;
use Symfony\Component\Validator\Exception\ValidatorException;

Expand Down Expand Up @@ -48,6 +57,45 @@ public function testCloudAppFromLocalConfig(): void
$this->executeCommand();
}

/**
* Test getIdeHelperText returns the expected IDE helper string.
* Calling from test (outside class) ensures the method remains public.
*/
public function testGetIdeHelperText(): void
{
$this->assertSame('This command will only work in an IDE terminal.', CommandBase::getIdeHelperText());
}

/**
* Test that all IDE-only commands append the IDE helper text to their help.
* Covers configure() appendHelp(CommandBase::getIdeHelperText()) in each command.
*/
public function testIdeCommandsHelpContainsIdeHelperText(): void
{
$ideHelperText = 'This command will only work in an IDE terminal.';
$commandClasses = [
IdePhpVersionCommand::class,
IdeServiceRestartCommand::class,
IdeServiceStartCommand::class,
IdeServiceStopCommand::class,
IdeShareCommand::class,
IdeXdebugToggleCommand::class,
IdeWizardCreateSshKeyCommand::class,
IdeWizardDeleteSshKeyCommand::class,
];

IdeHelper::setCloudIdeEnvVars();
try {
foreach ($commandClasses as $commandClass) {
$command = $this->injectCommand($commandClass);
$help = $command->getHelp();
$this->assertStringContainsString($ideHelperText, $help, $commandClass . ' help should contain IDE helper text.');
}
} finally {
IdeHelper::unsetCloudIdeEnvVars();
}
}

/**
* @return string[][]
*/
Expand Down
6 changes: 6 additions & 0 deletions tests/phpunit/src/Commands/Ide/IdePhpVersionCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ public function testIdePhpVersionCommandOutsideIde(): void
]);
}

public function testIdePhpVersionCommandHelpContainsIdeHelperText(): void
{
$help = $this->command->getHelp();
$this->assertStringContainsString('This command will only work in an IDE terminal.', $help);
}

protected function mockRestartPhp(ObjectProphecy|LocalMachineHelper $localMachineHelper): ObjectProphecy
{
$process = $this->prophet->prophesize(Process::class);
Expand Down
Loading