diff --git a/src/Command/BaseCommand.php b/src/Command/BaseCommand.php index 2299e6e..687d026 100644 --- a/src/Command/BaseCommand.php +++ b/src/Command/BaseCommand.php @@ -78,11 +78,11 @@ protected function preExecute(InputInterface $input, OutputInterface $output) { } if ($group = $this->getDrallGroup($input)) { - $this->logger->debug('Detected group: {group}', ['group' => $group]); + $this->logger->info('Using group: {group}', ['group' => $group]); } if ($filter = $this->getDrallFilter($input)) { - $this->logger->debug('Detected filter: {filter}', ['filter' => $filter]); + $this->logger->info('Using filter: {filter}', ['filter' => $filter]); } } diff --git a/src/Command/ExecCommand.php b/src/Command/ExecCommand.php index 28a9b4c..757c59f 100644 --- a/src/Command/ExecCommand.php +++ b/src/Command/ExecCommand.php @@ -12,6 +12,7 @@ use Drall\Model\EnvironmentId; use Drall\Model\Placeholder; use Drall\Trait\SignalAwareTrait; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -91,15 +92,38 @@ protected function configure() { } protected function initialize(InputInterface $input, OutputInterface $output): void { + if (!method_exists($input, 'getRawTokens')) { + parent::initialize($input, $output); + return; + } + + // Parts of the command after "exec". + $rawTokens = $input->getRawTokens(TRUE); + // If obsolete --drall-* options are present, then abort. - if (method_exists($input, 'getRawTokens')) { - foreach ($input->getRawTokens() as $token) { - if (str_starts_with($token, '--drall-')) { - $output->writeln(<<writeln(<<writeln(<<Incorrect: drall exec --dry-run drush --field=site core:status +Correct: drall exec --dry-run -- drush --field=site core:status + +Notice the `--` between `--dry-run` and the word `drush`. +EOT); + throw new \RuntimeException('Missing options separator'); } } } @@ -140,19 +164,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Display commands without executing them. if ($input->getOption('dry-run')) { foreach ($values as $value) { - $sCommand = Placeholder::replace([$placeholder->value => $value], $command); - $output->writeln("# Item: $value", OutputInterface::VERBOSITY_VERBOSE); - $output->writeln($sCommand); + $pCommand = Placeholder::replace([$placeholder->value => $value], $command); + $output->writeln("• $value: Preview"); + $output->writeln($pCommand, OutputInterface::VERBOSITY_QUIET); } - return 0; + return Command::SUCCESS; } $progressBar = new ProgressBar( $this->isProgressBarHidden($input) ? new NullOutput() : $output, count($values) ); - $exitCode = 0; + $exitCode = Command::SUCCESS; // Handle interruption signals to stop Drall gracefully. $isStopping = FALSE; @@ -162,7 +186,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int // If a previous SIGINT was received, then stop immediately. if ($isStopping) { $this->logger->error('Interrupted by user.'); - exit(1); + exit(Command::FAILURE); } // Prepare to stop after the current item is processed. @@ -194,14 +218,22 @@ function ($value) use ($command, $placeholder, $output, $progressBar, &$exitCode yield $process->start(); $this->logger->debug('Running: {command}', ['command' => $sCommand]); - $sOutput = yield ByteStream\buffer($process->getStdout()); - if (0 !== yield $process->join()) { - $exitCode = 1; + // @todo Improve formatting of headings. + $pOutput = yield ByteStream\buffer($process->getStdout()); + $pStatus = 'Done'; + $pIcon = '✔'; + if (Command::SUCCESS !== yield $process->join()) { + $pStatus = 'Failed'; + $pIcon = '✖'; + $exitCode = Command::FAILURE; } + $pMessage = "$pIcon $value: $pStatus"; + $progressBar->clear(); - $output->writeln("Finished: $value"); - $output->write($sOutput); + // Always display command output, even in --quiet mode. + $output->writeln($pMessage, OutputInterface::VERBOSITY_QUIET); + $output->write($pOutput); $progressBar->advance(); $progressBar->display(); @@ -217,7 +249,7 @@ function ($value) use ($command, $placeholder, $output, $progressBar, &$exitCode if ($isStopping) { $this->logger->error('Interrupted by user.'); - return 1; + return Command::FAILURE; } return $exitCode; @@ -241,29 +273,10 @@ function ($value) use ($command, $placeholder, $output, $progressBar, &$exitCode * Output: drush st --fields=site */ private function getCommand(InputInterface $input, OutputInterface $output): ?string { - $rawTokens = $input->getRawTokens(TRUE); - if (!in_array('--', $rawTokens)) { - foreach ($rawTokens as $token) { - if (str_starts_with($token, '-')) { - $output->writeln(<<Incorrect: drall exec --dry-run drush --field=site core:status -Correct: drall exec --dry-run -- drush --field=site core:status - -Notice the `--` between `--dry-run` and the word `drush`. -EOT); - $this->logger->error('Separator "--" must be used when using options.'); - return NULL; - } - } - } - - // @todo Throw an error if --drall-* options are present. // Everything after the first "--" is treated as an argument. All such // arguments are treated as parts of the command to be executed. $command = implode(' ', $input->getArguments()['cmd']); - $this->logger->debug("Command: {command}", ['command' => $command]); + $this->logger->debug("Command received: {command}", ['command' => $command]); if ( str_contains($command, 'drush') && @@ -272,6 +285,7 @@ private function getCommand(InputInterface $input, OutputInterface $output): ?st // Inject --uri=@@dir for Drush commands without placeholders. $command = preg_replace('/\b(drush) /', 'drush --uri=@@dir ', $command, -1); $this->logger->debug('Injected --uri parameter for Drush command.'); + $this->logger->notice("Command modified: {command}", ['command' => $command]); } return $command; diff --git a/src/Drall.php b/src/Drall.php index d89d7d4..c6182ba 100644 --- a/src/Drall.php +++ b/src/Drall.php @@ -42,15 +42,12 @@ public function __construct() { protected function configureIO(InputInterface $input, OutputInterface $output): void { parent::configureIO($input, $output); - if ($input->hasParameterOption('--debug', TRUE)) { + if ( + $input->hasParameterOption('--debug', TRUE) || + $input->hasParameterOption('-d', TRUE) + ) { $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); } - elseif ($input->hasParameterOption('--verbose', TRUE)) { - $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); - } - else { - $output->setVerbosity(OutputInterface::VERBOSITY_NORMAL); - } // The parent::configureIO sets verbosity in a SHELL_VERBOSITY. This causes // other Symfony Console apps to become verbose, for example, Drush. To @@ -69,7 +66,6 @@ protected function getDefaultInputDefinition(): InputDefinition { // Remove unneeded options. $options = $definition->getOptions(); unset( - $options['quiet'], $options['no-interaction'], ); $definition->setOptions($options); diff --git a/src/TestCase.php b/src/TestCase.php index 1bd141b..dff34cc 100644 --- a/src/TestCase.php +++ b/src/TestCase.php @@ -20,6 +20,19 @@ abstract class TestCase extends TestCaseBase { const PATH_EMPTY_DRUPAL = '/opt/empty-drupal'; + /** + * Normalizes console output by stripping unimportant spaces. + * + * @param string $output + * Raw output. + * + * @return string + * Normalized output. + */ + private static function normalizeString(string $output): string { + return preg_replace('@(\s+)\n@', "\n", $output); + } + /** * Creates a temporary file path. * @@ -57,19 +70,16 @@ protected function createDrupalFinderStub(?string $root = NULL): DrupalFinderCom return $drupalFinder; } - /** - * Asserts Shell output ignoring unimportant whitespace. - * - * @param string $expected - * Expected output. - * @param mixed $actual - * Actual output. - * @param string $message - * Error message. - */ protected function assertOutputEquals(string $expected, mixed $actual, string $message = ''): void { - $actual = preg_replace('@(\s+)\n@', "\n", $actual ?? ''); - $this->assertEquals($expected, $actual, $message); + $this->assertEquals($expected, self::normalizeString($actual ?? ''), $message); + } + + protected function assertOutputStartsWith(string $expected, mixed $actual, string $message = ''): void { + $this->assertStringStartsWith($expected, self::normalizeString($actual ?? ''), $message); + } + + protected function assertOutputContainsString(string $expected, mixed $actual, string $message = ''): void { + $this->assertStringContainsString($expected, self::normalizeString($actual ?? ''), $message); } } diff --git a/test/Integration/Command/BaseCommandTest.php b/test/Integration/Command/BaseCommandTest.php new file mode 100644 index 0000000..b4794c3 --- /dev/null +++ b/test/Integration/Command/BaseCommandTest.php @@ -0,0 +1,62 @@ +run(); + $this->assertOutputStartsWith(<<getOutput()); + + // Short form. + $process1 = Process::fromShellCommandline( + 'drall exec -g bluish --dry-run -vv -- ./vendor/bin/drush st', + static::PATH_DRUPAL, + ); + $process1->run(); + $this->assertOutputStartsWith(<<getOutput()); + } + + /** + * @testdox Detects --filter. + */ + public function testWithFilter(): void { + $process1 = Process::fromShellCommandline( + 'drall exec --filter=leo --dry-run -vv -- ./vendor/bin/drush st', + static::PATH_DRUPAL, + ); + $process1->run(); + $this->assertOutputStartsWith(<<getOutput()); + + // Short form. + $process2 = Process::fromShellCommandline( + 'drall exec -f leo --dry-run -vv -- ./vendor/bin/drush st', + static::PATH_DRUPAL, + ); + $process2->run(); + $this->assertOutputStartsWith(<<getOutput()); + } + +} diff --git a/test/Integration/Command/ExecCommandTest.php b/test/Integration/Command/ExecCommandTest.php index 2f80bea..4c9ecf4 100644 --- a/test/Integration/Command/ExecCommandTest.php +++ b/test/Integration/Command/ExecCommandTest.php @@ -12,38 +12,24 @@ class ExecCommandTest extends TestCase { /** - * @testdox Detects commands correctly. + * @testdox Works when -- is absent and options are not used. */ - public function testCommandDetection() { + public function testMissingOptionsSeparatorWithNoOptions(): void { $process = Process::fromShellCommandline( - 'drall exec --debug --dry-run -- drush st', + 'drall exec ./vendor/bin/drush st', static::PATH_DRUPAL, ); $process->run(); - $this->assertEquals(<<getOutput()); + $this->assertEquals(0, $process->getExitCode()); } /** - * @testdox Works when -- is absent and options are not used. + * @testdox Shows error when -- is absent but options are used. */ - public function testMissingArgsSeparatorWithNoOptions(): void { + public function testMissingOptionsSeparatorWithOptions(): void { $process = Process::fromShellCommandline( 'drall exec --dry-run drush st', - static::PATH_DRUPAL, + static::PATH_DRUPAL, ); $process->run(); $this->assertOutputEquals(<<getOutput()); + $this->assertOutputContainsString('Missing options separator', $process->getErrorOutput()); + $this->assertEquals(1, $process->getExitCode()); } /** - * @testdox Shows error when -- is absent but options are used. + * @testdox Shows error when --drall-* options are detected. */ - public function testMissingArgsSeparatorWithOptions(): void { - $process = Process::fromShellCommandline( - 'drall exec --dry-run drush st', - static::PATH_DRUPAL, - ); + public function testShowErrorForObsoleteOptions(): void { + $process = Process::fromShellCommandline('./vendor/bin/drall exec --drall-foo drush st', static::PATH_DRUPAL); $process->run(); $this->assertOutputEquals(<<getOutput()); + $this->assertOutputContainsString('Obsolete options detected', $process->getErrorOutput()); + $this->assertEquals(1, $process->getExitCode()); } /** @@ -133,7 +118,7 @@ public function testWorkingDirectory(): void { ); $process->run(); $this->assertOutputEquals(<<run(); $this->assertOutputEquals(<<getOutput()); @@ -174,15 +159,15 @@ public function testDrushWithSitePlaceholder(): void { ); $process->run(); $this->assertOutputEquals(<<getOutput()); @@ -198,15 +183,15 @@ public function testDrushWithNoPlaceholders(): void { ); $process->run(); $this->assertOutputEquals(<<getOutput()); @@ -222,19 +207,19 @@ public function testMultipleDrushWithNoPlaceholders(): void { ); $process->run(); $this->assertOutputEquals(<<run(); $this->assertOutputEquals(<<run(); $this->assertOutputEquals(<<getOutput()); @@ -337,7 +322,7 @@ public function testWithFilter(): void { ); $process1->run(); $this->assertOutputEquals(<<getOutput()); @@ -349,7 +334,7 @@ public function testWithFilter(): void { ); $process2->run(); $this->assertOutputEquals(<<getOutput()); @@ -365,21 +350,21 @@ public function testWithDirPlaceholderAndDebug(): void { ); $process->run(); $this->assertOutputEquals(<<getOutput()); @@ -395,9 +380,9 @@ public function testWithGroup(): void { ); $process1->run(); $this->assertOutputEquals(<<getOutput()); @@ -409,9 +394,9 @@ public function testWithGroup(): void { ); $process2->run(); $this->assertOutputEquals(<<getOutput()); @@ -428,9 +413,9 @@ public function testWithGroupEnvVar(): void { ); $process->run(); $this->assertOutputEquals(<<getOutput()); @@ -446,15 +431,15 @@ public function testWithSitePlaceholder(): void { ); $process->run(); $this->assertOutputEquals(<<getOutput()); @@ -470,21 +455,21 @@ public function testWithSitePlaceholderDebug(): void { ); $process->run(); $this->assertOutputEquals(<<getOutput()); @@ -500,9 +485,9 @@ public function testWithSitePlaceholderAndGroup(): void { ); $process->run(); $this->assertOutputEquals(<<getOutput()); @@ -522,7 +507,7 @@ public function testCatchStdErrOutput(): void { $output = preg_replace('@(Drush version :) ([\d|\.|-]+)@', '$1 x.y.z', $process->getOutput()); $this->assertOutputEquals(<<&1', static::PATH_DRUPAL, @@ -542,15 +527,15 @@ public function testWithProgressBarVisible(): void { ); $process->run(); $this->assertOutputEquals(<<----------------------] 20%Finished: donnie + 1/5 [=====>----------------------] 20%✔ donnie: Done sites/donnie - 2/5 [===========>----------------] 40%Finished: leo + 2/5 [===========>----------------] 40%✔ leo: Done sites/leo - 3/5 [================>-----------] 60%Finished: mikey + 3/5 [================>-----------] 60%✔ mikey: Done sites/mikey - 4/5 [======================>-----] 80%Finished: ralph + 4/5 [======================>-----] 80%✔ ralph: Done sites/ralph 5/5 [============================] 100% @@ -560,7 +545,7 @@ public function testWithProgressBarVisible(): void { /** * @testdox With --no-progress. */ - public function testWithProgressBarHidden(): void { + public function testWithNoProgressBar(): void { $process = Process::fromShellCommandline( 'drall exec --no-progress -- ./vendor/bin/drush st --field=site 2>&1', static::PATH_DRUPAL, @@ -572,20 +557,54 @@ public function testWithProgressBarHidden(): void { ); $process->run(); $this->assertOutputEquals(<<getOutput()); } + /** + * @testdox With verbosity quiet. + */ + public function testWithVerbosityQuiet(): void { + $process1 = Process::fromShellCommandline( + 'drall exec --quiet -- ./vendor/bin/drush st --field=site', + static::PATH_DRUPAL, + ); + $process1->run(); + $this->assertEquals(<<getOutput()); + + // Short form. + $process2 = Process::fromShellCommandline( + 'drall exec -q -- ./vendor/bin/drush st --field=site', + static::PATH_DRUPAL, + ); + $process2->run(); + $this->assertEquals(<<getOutput()); + } + /** * @testdox With --dry-run. */ @@ -596,10 +615,15 @@ public function testWithDryRun(): void { ); $process1->run(); $this->assertOutputEquals(<<getOutput()); @@ -611,35 +635,35 @@ public function testWithDryRun(): void { ); $process2->run(); $this->assertOutputEquals(<<getOutput()); } /** - * @testdox With --dry-run --verbose. + * @testdox With --dry-run --quiet. */ - public function testWithDryRunVerbose(): void { + public function testWithDryRunQuiet(): void { $process = Process::fromShellCommandline( - 'drall exec --dry-run --verbose -- drush st', + 'drall exec --dry-run --quiet -- ./vendor/bin/drush st', static::PATH_DRUPAL, ); $process->run(); $this->assertOutputEquals(<<getOutput()); } @@ -690,13 +714,26 @@ public function testWorkerLimit(): void { } /** - * @testdox Non-zero exit code. + * @testdox Exits with non-zero code if any command fails. */ public function testNonZeroExitCode(): void { $process = Process::fromShellCommandline( - 'drall exec --group=bad ./vendor/bin/drush st --field=site', + "drall ex -- \"if [ 'default' = '@@dir' ]; then exit 1; fi; echo 'Hello @@dir!';\"", + static::PATH_DRUPAL, ); $process->run(); + $this->assertOutputEquals(<<getOutput()); $this->assertEquals(1, $process->getExitCode()); } diff --git a/test/Integration/DrallTest.php b/test/Integration/DrallTest.php index 2f55e63..feb12c0 100644 --- a/test/Integration/DrallTest.php +++ b/test/Integration/DrallTest.php @@ -38,18 +38,4 @@ public function testUnrecognizedCommand(): void { EOT, $process->getErrorOutput()); } - /** - * @testdox Shows error when --drall-* options are detected. - */ - public function testShowErrorForObsoleteOptions(): void { - $process = Process::fromShellCommandline('./vendor/bin/drall exec --drall-foo drush st', static::PATH_DRUPAL); - $process->run(); - $this->assertOutputEquals(<<getOutput()); - $this->assertEquals(1, $process->getExitCode()); - } - } diff --git a/test/Unit/DrallTest.php b/test/Unit/DrallTest.php index 8396228..c8ab2d3 100644 --- a/test/Unit/DrallTest.php +++ b/test/Unit/DrallTest.php @@ -17,12 +17,16 @@ public function testName() { $this->assertSame(Drall::NAME, $app->getName()); } + /** + * @testdox Default input options are defined. + */ public function testDefaultInputOptions() { $app = new Drall(); $options = $app->getDefinition()->getOptions(); $this->assertEquals([ 'help', + 'quiet', 'verbose', 'version', 'ansi', @@ -30,20 +34,69 @@ public function testDefaultInputOptions() { ], array_keys($options)); } - public function testOptionVerbosityNormal() { + /** + * @testdox Verbosity quiet. + */ + public function testVerbosityQuiet() { + $tester = new ApplicationTester(new Drall()); + + $tester->run(['command' => 'version', '--quiet' => TRUE]); + $this->assertEquals(OutputInterface::VERBOSITY_QUIET, $tester->getOutput()->getVerbosity()); + + $tester->run(['command' => 'version', '-q' => TRUE]); + $this->assertEquals(OutputInterface::VERBOSITY_QUIET, $tester->getOutput()->getVerbosity()); + } + + /** + * @testdox Verbosity level 0. + */ + public function testVerbosityNormal() { $tester = new ApplicationTester(new Drall()); $tester->run(['command' => 'version']); $this->assertEquals(OutputInterface::VERBOSITY_NORMAL, $tester->getOutput()->getVerbosity()); } - public function testOptionVerbosityVerbose() { + /** + * @testdox Verbosity level 1. + */ + public function testVerbosityVerbose() { $tester = new ApplicationTester(new Drall()); + $tester->run(['command' => 'version', '--verbose' => TRUE]); + $this->assertEquals(OutputInterface::VERBOSITY_VERBOSE, $tester->getOutput()->getVerbosity()); + + $tester->run(['command' => 'version', '-v' => TRUE]); + $this->assertEquals(OutputInterface::VERBOSITY_VERBOSE, $tester->getOutput()->getVerbosity()); + } + + /** + * @testdox Verbosity level 2. + */ + public function testVerbosityVeryVerbose() { + $tester = new ApplicationTester(new Drall()); + + $tester->run(['command' => 'version', '--verbose' => 2]); + $this->assertEquals(OutputInterface::VERBOSITY_VERY_VERBOSE, $tester->getOutput()->getVerbosity()); + + $tester->run(['command' => 'version', '-vv' => TRUE]); $this->assertEquals(OutputInterface::VERBOSITY_VERY_VERBOSE, $tester->getOutput()->getVerbosity()); } - public function testOptionVerbosityDebug() { + /** + * @testdox Verbosity level 3. + */ + public function testVerbosityDebug() { $tester = new ApplicationTester(new Drall()); + + $tester->run(['command' => 'version', '--verbose' => 3]); + $this->assertEquals(OutputInterface::VERBOSITY_DEBUG, $tester->getOutput()->getVerbosity()); + + $tester->run(['command' => 'version', '-vvv' => TRUE]); + $this->assertEquals(OutputInterface::VERBOSITY_DEBUG, $tester->getOutput()->getVerbosity()); + + $tester->run(['command' => 'version', '-d']); + $this->assertEquals(OutputInterface::VERBOSITY_DEBUG, $tester->getOutput()->getVerbosity()); + $tester->run(['command' => 'version', '--debug' => TRUE]); $this->assertEquals(OutputInterface::VERBOSITY_DEBUG, $tester->getOutput()->getVerbosity()); }