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
2 changes: 1 addition & 1 deletion .github/wiki
Submodule wiki updated from f1a8bc to 4782e8
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Show the DevTools ASCII logo by default on all top-level command executions, while adding a `--no-logo` global option and automatically suppressing the banner for `--json` / `--pretty-json` invocations (including automatic forwarding of `--no-logo` to internal DevTools subprocesses) to avoid banner repetition in orchestrated command queues (#277)

### Added

- Add a configurable DevTools generated artifact workspace through `--workspace-dir` and `FAST_FORWARD_WORKSPACE_DIR`, keeping explicit output/cache command options authoritative (#274)
Expand Down
29 changes: 17 additions & 12 deletions src/Console/DevTools.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ final class DevTools extends Application
| | | |/ _ \ \ / / | |/ _ \ / _ \| / __|
| |_| | __/\ V / | | (_) | (_) | \__ \
|____/ \___| \_/ |_|\___/ \___/|_|___/
========================================

LOGO;

/**
Expand Down Expand Up @@ -87,17 +89,6 @@ public function __construct(
$this->setCommandLoader($commandLoader);
}

/**
* Gets the help message for the DevTools application, including the ASCII logo.
*
* @return string
*/
#[Override]
public function getHelp(): string
{
return self::LOGO . "\n\n" . parent::getHelp();
}

/**
* Returns the application-level input definition with DevTools runtime options.
*
Expand Down Expand Up @@ -128,6 +119,12 @@ protected function getDefaultInputDefinition(): InputDefinition
description: 'Store generated DevTools artifacts in the given directory.',
));

$definition->addOption(new InputOption(
name: 'no-logo',
mode: InputOption::VALUE_NONE,
description: 'Hide the startup ASCII logo.',
));

return $definition;
}

Expand All @@ -142,6 +139,14 @@ protected function getDefaultInputDefinition(): InputDefinition
#[Override]
public function doRun(InputInterface $input, OutputInterface $output): int
{
$noLogo = (bool) $input->getParameterOption('--no-logo', null, true)
|| (bool) $input->hasParameterOption('--json', true)
|| (bool) $input->hasParameterOption('--pretty-json', true);

if (! $noLogo) {
$output->writeln(self::LOGO);
}

try {
$this->workingDirectorySwitcher->switchTo($this->getWorkingDirectoryOption($input));
$this->configureWorkspaceDirectory($input);
Expand All @@ -151,7 +156,7 @@ public function doRun(InputInterface $input, OutputInterface $output): int
return Command::FAILURE;
}

if (! $this->isSelfUpdateCommand($input)) {
if (! $noLogo && ! $this->isSelfUpdateCommand($input)) {
$this->runAutoUpdateWhenRequested($input, $output);
$this->versionCheckNotifier->notify($output);
}
Expand Down
46 changes: 46 additions & 0 deletions src/Process/ProcessBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

namespace FastForward\DevTools\Process;

use FastForward\DevTools\Path\DevToolsPathResolver;
use Symfony\Component\Process\Process;

/**
Expand All @@ -31,6 +32,8 @@
*/
final readonly class ProcessBuilder implements ProcessBuilderInterface
{
private const string NO_LOGO_ARGUMENT = '--no-logo';

/**
* Creates a new immutable process builder instance.
*
Expand Down Expand Up @@ -94,10 +97,53 @@ public function getArguments(): array
*/
public function build(string|array $command): Process
{
if (\is_array($command)) {
$command = array_values($command);
}

if (\is_string($command)) {
$command = explode(' ', $command);
}

if ($this->shouldAddLogoSuppressionArgument($command)) {
$command = $this->prependLogoSuppressionArgument($command);
}

return new Process(command: [...$command, ...$this->arguments], timeout: 0);
}

/**
* @param list<string> $command
*/
private function shouldAddLogoSuppressionArgument(array $command): bool
{
if (\in_array(self::NO_LOGO_ARGUMENT, $this->arguments, true)) {
return false;
}

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

$binary = str_replace('\\', '/', $command[0]);
$packageBinaryPath = str_replace('\\', '/', DevToolsPathResolver::getBinaryPath());

return $binary === $packageBinaryPath;
}

/**
* @param list<string> $command
*
* @return list<string>
*/
private function prependLogoSuppressionArgument(array $command): array
{
if ([] === $command) {
return $command;
}

$binary = array_shift($command);

return [$binary, self::NO_LOGO_ARGUMENT, ...$command];
}
}
2 changes: 2 additions & 0 deletions tests/Console/Command/DependenciesCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use FastForward\DevTools\Console\Command\DependenciesCommand;
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
use FastForward\DevTools\Config\ComposerDependencyAnalyserConfig;
use FastForward\DevTools\Path\DevToolsPathResolver;
use FastForward\DevTools\Process\ProcessBuilder;
use FastForward\DevTools\Process\ProcessBuilderInterface;
use FastForward\DevTools\Process\ProcessQueueInterface;
Expand All @@ -42,6 +43,7 @@
use Symfony\Component\Process\Process;

#[CoversClass(DependenciesCommand::class)]
#[UsesClass(DevToolsPathResolver::class)]
#[UsesClass(ProcessBuilder::class)]
#[UsesTrait(LogsCommandResults::class)]
final class DependenciesCommandTest extends TestCase
Expand Down
2 changes: 2 additions & 0 deletions tests/Console/Command/TestsCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use FastForward\DevTools\Process\ProcessBuilder;
use FastForward\DevTools\Process\ProcessQueueInterface;
use FastForward\DevTools\Path\ManagedWorkspace;
use FastForward\DevTools\Path\DevToolsPathResolver;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
Expand All @@ -48,6 +49,7 @@

#[CoversClass(TestsCommand::class)]
#[UsesClass(CoverageSummary::class)]
#[UsesClass(DevToolsPathResolver::class)]
#[UsesClass(ProcessBuilder::class)]
#[UsesClass(ManagedWorkspace::class)]
#[UsesTrait(LogsCommandResults::class)]
Expand Down
158 changes: 158 additions & 0 deletions tests/Console/DevToolsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,11 @@
use Symfony\Component\Console\Command\HelpCommand;
use Symfony\Component\Console\Command\ListCommand;
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\BufferedOutput;

use function Safe\putenv;

Expand Down Expand Up @@ -215,6 +218,148 @@ public function constructorWillRegisterGlobalRuntimeOptions(): void
self::assertTrue($definition->hasOption('workspace-dir'));
self::assertSame('w', $definition->getOption('workspace-dir')->getShortcut());
self::assertTrue($definition->hasOption('auto-update'));
self::assertTrue($definition->hasOption('no-logo'));
self::assertFalse($definition->getOption('no-logo')->acceptValue());
}

/**
* @return void
*/
#[Test]
public function doRunWillRenderLogoUnlessNoLogoOptionIsProvided(): void
{
$input = new ArrayInput([
'command' => 'list',
]);

$output = new BufferedOutput();

$this->environment->get('FAST_FORWARD_AUTO_UPDATE', '')
->willReturn('');
$this->workingDirectorySwitcher->switchTo(null)
->shouldBeCalledOnce();
$this->versionCheckNotifier->notify($output)
->shouldBeCalledOnce();

$result = $this->invokeDoRun($input, $output);

self::assertSame(Command::SUCCESS, $result);
self::assertStringContainsString('_____', $output->fetch());
}

/**
* @return void
*/
#[Test]
public function doRunWillNotRenderLogoWhenNoLogoOptionIsSet(): void
{
$input = new ArrayInput([
'--no-logo' => true,
'command' => 'list',
]);

$output = new BufferedOutput();

$this->environment->get('FAST_FORWARD_AUTO_UPDATE', '')
->willReturn('');
$this->workingDirectorySwitcher->switchTo(null)
->shouldBeCalledOnce();
$this->versionCheckNotifier->notify($output)
->shouldNotBeCalled();

$this->invokeDoRun($input, $output);

self::assertStringNotContainsString('_____', $output->fetch());
}

/**
* @return void
*/
#[Test]
public function doRunWillNotRenderLogoWhenJsonOptionIsProvided(): void
{
$command = new class extends Command {
public function __construct()
{
parent::__construct('standards');
}

protected function configure(): void
{
$this->addOption(name: 'json', mode: InputOption::VALUE_NONE, description: 'Emit structured JSON output.');
$this->setCode(static fn(InputInterface $input, OutputInterface $output): int => Command::SUCCESS);
}
};

$this->commandLoader->has('standards')
->willReturn(true)
->shouldBeCalledOnce();
$this->commandLoader->get('standards')
->willReturn($command)
->shouldBeCalledOnce();
$input = new ArrayInput([
'command' => 'standards',
'--json' => true,
]);

$output = new BufferedOutput();

$this->environment->get('FAST_FORWARD_AUTO_UPDATE', '')
->willReturn('');
$this->workingDirectorySwitcher->switchTo(null)
->shouldBeCalledOnce();
$this->versionCheckNotifier->notify($output)
->shouldNotBeCalled();

$result = $this->invokeDoRun($input, $output);

self::assertSame(Command::SUCCESS, $result);
self::assertStringNotContainsString('_____', $output->fetch());
}

/**
* @return void
*/
#[Test]
public function doRunWillNotRenderLogoWhenPrettyJsonOptionIsProvided(): void
{
$command = new class extends Command {
public function __construct()
{
parent::__construct('standards');
}

protected function configure(): void
{
$this->addOption(name: 'pretty-json', mode: InputOption::VALUE_NONE, description: 'Emit pretty JSON output.');
$this->setCode(static fn(InputInterface $input, OutputInterface $output): int => Command::SUCCESS);
}
};

$this->commandLoader->has('standards')
->willReturn(true)
->shouldBeCalledOnce();
$this->commandLoader->get('standards')
->willReturn($command)
->shouldBeCalledOnce();
$input = new ArrayInput([
'command' => 'standards',
'--pretty-json' => true,
]);

$output = new BufferedOutput();

$this->environment->get('FAST_FORWARD_AUTO_UPDATE', '')
->willReturn('');
$this->workingDirectorySwitcher->switchTo(null)
->shouldBeCalledOnce();
$this->versionCheckNotifier->notify($output)
->shouldNotBeCalled();

$result = $this->invokeDoRun($input, $output);

self::assertSame(Command::SUCCESS, $result);
self::assertStringNotContainsString('_____', $output->fetch());
}

/**
Expand Down Expand Up @@ -404,4 +549,17 @@ private function invokeConfigureWorkspaceDirectory(InputInterface $input): void
$reflectionMethod = new ReflectionMethod($this->devTools, 'configureWorkspaceDirectory');
$reflectionMethod->invoke($this->devTools, $input);
}

/**
* @param InputInterface $input
* @param OutputInterface $output
*
* @return int
*/
private function invokeDoRun(InputInterface $input, OutputInterface $output): int
{
$reflectionMethod = new ReflectionMethod($this->devTools, 'doRun');

return (int) $reflectionMethod->invoke($this->devTools, $input, $output);
}
}
Loading
Loading