diff --git a/Makefile b/Makefile index beffb79..d91606a 100644 --- a/Makefile +++ b/Makefile @@ -85,9 +85,9 @@ provision/drall: # Drall, it must be re-installed inside the Drupal installation. .PHONY: refresh refresh: - rsync -Ervu --inplace --delete --exclude=.coverage --exclude=.phpunit.cache --exclude=.idea --exclude=.git --exclude=vendor /opt/drall/ /opt/no-drupal/vendor/jigarius/drall/ - rsync -Ervu --inplace --delete --exclude=.coverage --exclude=.phpunit.cache --exclude=.idea --exclude=.git --exclude=vendor /opt/drall/ /opt/empty-drupal/vendor/jigarius/drall/ - rsync -Ervu --inplace --delete --exclude=.coverage --exclude=.phpunit.cache --exclude=.idea --exclude=.git --exclude=vendor /opt/drall/ /opt/drupal/vendor/jigarius/drall/ + rsync -Ervu --inplace --delete --exclude=.coverage --exclude=.phpunit.cache --exclude=.idea --exclude=.git --exclude=test --exclude=vendor /opt/drall/ /opt/no-drupal/vendor/jigarius/drall/ + rsync -Ervu --inplace --delete --exclude=.coverage --exclude=.phpunit.cache --exclude=.idea --exclude=.git --exclude=test --exclude=vendor /opt/drall/ /opt/empty-drupal/vendor/jigarius/drall/ + rsync -Ervu --inplace --delete --exclude=.coverage --exclude=.phpunit.cache --exclude=.idea --exclude=.git --exclude=test --exclude=vendor /opt/drall/ /opt/drupal/vendor/jigarius/drall/ .PHONY: coverage-report/text @@ -107,7 +107,7 @@ lint: .PHONY: test test: - DRALL_ENVIRONMENT=test XDEBUG_MODE=coverage composer --working-dir=/opt/drall run test + XDEBUG_MODE=coverage composer --working-dir=/opt/drall run test .PHONY: info diff --git a/src/Command/ExecCommand.php b/src/Command/ExecCommand.php index 81d86fa..328bc61 100644 --- a/src/Command/ExecCommand.php +++ b/src/Command/ExecCommand.php @@ -3,9 +3,9 @@ namespace Drall\Command; use Amp\ByteStream; +use Amp\ByteStream\WritableResourceStream; use Amp\Pipeline\Pipeline; use Amp\Process\Process; -use Drall\Model\EnvironmentId; use Drall\Model\Placeholder; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\SignalableCommandInterface; @@ -15,6 +15,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\StreamOutput; /** * A command to execute a shell command on multiple sites. @@ -83,9 +84,16 @@ protected function configure() { 'Do not execute commands, only display them.' ); + $this->addOption( + 'no-buffer', + 'B', + InputOption::VALUE_NONE, + 'Do not buffer output.' + ); + $this->addOption( 'no-progress', - NULL, + 'P', InputOption::VALUE_NONE, 'Do not show a progress bar.' ); @@ -191,9 +199,14 @@ protected function preExecute(InputInterface $input, OutputInterface $output): v if ($interval = $input->getOption('interval')) { $this->logger->notice("Using a $interval-second interval between commands.", ['interval' => $interval]); } + + if ($input->getOption('no-buffer')) { + $this->logger->notice("Using no output buffering."); + } } protected function execute(InputInterface $input, OutputInterface $output): int { + /** @var \Symfony\Component\Console\Output\ConsoleOutput $output */ $this->preExecute($input, $output); if (!$command = $this->getCommand($input, $output)) { @@ -225,25 +238,29 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($input->getOption('dry-run')) { foreach ($values as $value) { $pCommand = Placeholder::replace([$placeholder->value => $value], $command); - $output->writeln("• $value: Preview"); - $output->writeln($pCommand, OutputInterface::VERBOSITY_QUIET); + $output->writeln($pCommand); } return Command::SUCCESS; } + $textSection = $output->section(); $progressBar = new ProgressBar( - $this->isProgressBarHidden($input) ? new NullOutput() : $output, + $input->getOption('no-progress') ? new NullOutput() : $output->section(), count($values) ); + $exitCode = Command::SUCCESS; + // Within the iteration, all output must go through the output sections. + // This keeps the text at the top and the progress bar at the bottom. Pipeline::fromIterable($values) ->concurrent($input->getOption('workers')) ->unordered() ->forEach((function ($value) use ( $input, $output, + $textSection, $command, $placeholder, $progressBar, @@ -255,27 +272,30 @@ protected function execute(InputInterface $input, OutputInterface $output): int $pCommand = Placeholder::replace([$placeholder->value => $value], $command); $process = Process::start("($pCommand) 2>&1"); - $this->logger->debug('Running: {command}', ['command' => $pCommand]); - - // @todo Improve formatting of headings. - $pOutput = ByteStream\buffer($process->getStdout()); - $pStatus = 'Done'; - $pIcon = '✔'; - if (Command::SUCCESS !== $process->join()) { - $pStatus = 'Failed'; - $pIcon = '✖'; - $exitCode = Command::FAILURE; - } - $pMessage = "$pIcon $value: $pStatus"; + // Send process output directly to the output stream. + if ( + $input->getOption('no-buffer') && + is_a($output, StreamOutput::class) + ) { + $wStream = new WritableResourceStream($output->getStream()); + ByteStream\pipe($process->getStdout(), $wStream); + } + // Buffer process output until it finishes. + elseif ($pOutput = rtrim(ByteStream\buffer($process->getStdout()))) { + // Always display command output, even in --quiet mode. + $textSection->writeln($pOutput, OutputInterface::VERBOSITY_QUIET); + } - $progressBar->clear(); - // Always display command output, even in --quiet mode. - $output->writeln($pMessage, OutputInterface::VERBOSITY_QUIET); - $output->write($pOutput); + if (Command::SUCCESS === $process->join()) { + $textSection->writeln("✔ $value: Done"); + } + else { + $textSection->writeln("✖ $value: Failed"); + $exitCode = Command::FAILURE; + } $progressBar->advance(); - $progressBar->display(); // Wait between commands if --interval is specified. if ($interval = $input->getOption('interval')) { @@ -289,7 +309,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $progressBar->finish(); - $output->writeln(''); return $exitCode; } @@ -348,26 +367,6 @@ private function getUniquePlaceholder(string $command): ?Placeholder { return reset($placeholders); } - /** - * Whether the Drall progress bar should be hidden. - * - * @param \Symfony\Component\Console\Input\InputInterface $input - * The input. - * - * @return bool - * True or false. - */ - private function isProgressBarHidden(InputInterface $input): bool { - if ( - EnvironmentId::Test->isActive() || - $input->getOption('no-progress') - ) { - return TRUE; - } - - return FALSE; - } - public function getSubscribedSignals(): array { return [SIGINT]; } diff --git a/src/Model/EnvironmentId.php b/src/Model/EnvironmentId.php deleted file mode 100644 index 9aff0f7..0000000 --- a/src/Model/EnvironmentId.php +++ /dev/null @@ -1,26 +0,0 @@ -value; - } - -} diff --git a/test/Integration/Command/ExecCommandTest.php b/test/Integration/Command/ExecCommandTest.php index 6736f40..183fd8e 100644 --- a/test/Integration/Command/ExecCommandTest.php +++ b/test/Integration/Command/ExecCommandTest.php @@ -2,11 +2,14 @@ namespace Drall\Test\Integration\Command; -use Drall\Model\EnvironmentId; use Drall\TestCase; use Symfony\Component\Process\Process; /** + * Test the `drall exec` command. + * + * Most of the tests disable the progress bar with the -P option. + * * @testdox exec command * @covers \Drall\Command\ExecCommand */ @@ -29,7 +32,7 @@ public function testMissingOptionsSeparatorWithNoOptions(): void { */ public function testMissingOptionsSeparatorWithOptions(): void { $process = Process::fromShellCommandline( - 'drall exec --dry-run drush st', + 'drall exec -P --dry-run drush st', static::PATH_DRUPAL, ); $process->run(); @@ -48,7 +51,7 @@ public function testMissingOptionsSeparatorWithOptions(): void { * @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 = Process::fromShellCommandline('./vendor/bin/drall exec -P --drall-foo drush st', static::PATH_DRUPAL); $process->run(); $this->assertOutputEquals(<<run(); $this->assertStringContainsString('Package "drupal/core" is not installed', $process->getErrorOutput()); $process = Process::fromShellCommandline( - 'drall exec ./vendor/bin/drush @@site.local core:status', + 'drall exec --no-progress -- ./vendor/bin/drush @@site.local core:status', static::PATH_NO_DRUPAL, ); $process->run(); @@ -83,14 +86,14 @@ public function testWithNoDrupal(): void { */ public function testWithEmptyDrupal(): void { $process = Process::fromShellCommandline( - 'drall exec -- ./vendor/bin/drush --uri=@@dir core:status', + 'drall exec --no-progress -- ./vendor/bin/drush --uri=@@dir core:status', static::PATH_EMPTY_DRUPAL, ); $process->run(); $this->assertOutputEquals('[warning] No Drupal sites found.' . PHP_EOL, $process->getOutput()); $process = Process::fromShellCommandline( - 'drall exec ./vendor/bin/drush @@site.local core:status', + 'drall exec --no-progress -- ./vendor/bin/drush @@site.local core:status', static::PATH_EMPTY_DRUPAL, ); $process->run(); @@ -101,7 +104,7 @@ public function testWithEmptyDrupal(): void { * @testdox Raises error for non-Drush command with no placeholders. */ public function testNonDrushWithNoPlaceholders(): void { - $process = Process::fromShellCommandline('drall exec foo', static::PATH_DRUPAL); + $process = Process::fromShellCommandline('drall exec --no-progress -- foo', static::PATH_DRUPAL); $process->run(); $this->assertOutputEquals( '[error] The command contains no placeholders. Please run it directly without Drall.' . PHP_EOL, @@ -114,14 +117,14 @@ public function testNonDrushWithNoPlaceholders(): void { */ public function testWorkingDirectory(): void { $process = Process::fromShellCommandline( - 'drall exec --filter=tmnt -- "echo \"Site: @@site\" && pwd"', + 'drall exec --no-progress --filter=tmnt -- "echo \"Site: @@site\" && pwd"', static::PATH_DRUPAL, ); $process->run(); $this->assertOutputEquals(<<getOutput()); } @@ -131,21 +134,21 @@ public function testWorkingDirectory(): void { */ public function testDrushWithUriPlaceholder(): void { $process = Process::fromShellCommandline( - 'drall exec -- ./vendor/bin/drush --uri=@@dir core:status --field=site', + 'drall exec --no-progress -- ./vendor/bin/drush --uri=@@dir core:status --field=site', static::PATH_DRUPAL, ); $process->run(); $this->assertOutputEquals(<<getOutput()); } @@ -155,21 +158,21 @@ public function testDrushWithUriPlaceholder(): void { */ public function testDrushWithSitePlaceholder(): void { $process = Process::fromShellCommandline( - 'drall exec ./vendor/bin/drush -- @@site.local core:status --field=site', + 'drall exec --no-progress -- ./vendor/bin/drush @@site.local core:status --field=site', static::PATH_DRUPAL, ); $process->run(); $this->assertOutputEquals(<<getOutput()); } @@ -179,21 +182,21 @@ public function testDrushWithSitePlaceholder(): void { */ public function testDrushWithNoPlaceholders(): void { $process = Process::fromShellCommandline( - 'drall exec -- ./vendor/bin/drush core:status --field=site', + 'drall exec --no-progress -- ./vendor/bin/drush core:status --field=site', static::PATH_DRUPAL, ); $process->run(); $this->assertOutputEquals(<<getOutput()); } @@ -203,26 +206,26 @@ public function testDrushWithNoPlaceholders(): void { */ public function testMultipleDrushWithNoPlaceholders(): void { $process = Process::fromShellCommandline( - 'drall exec "./vendor/bin/drush st --fields=site; ./vendor/bin/drush st --fields=uri"', + 'drall exec --no-progress -- "./vendor/bin/drush st --fields=site; ./vendor/bin/drush st --fields=uri"', static::PATH_DRUPAL, ); $process->run(); $this->assertOutputEquals(<<getOutput()); } @@ -232,7 +235,7 @@ public function testMultipleDrushWithNoPlaceholders(): void { */ public function testDrushInPath(): void { $process = Process::fromShellCommandline( - 'drall exec ls ./vendor/drush/src', + 'drall exec --no-progress -- ls ./vendor/drush/src', static::PATH_DRUPAL, ); $process->run(); @@ -250,26 +253,26 @@ public function testDrushInPath(): void { */ public function testDrushCapitalized(): void { $process = Process::fromShellCommandline( - 'drall exec "echo \"Drush status\" && ./vendor/bin/drush st --fields=site"', + 'drall exec --no-progress -- "echo \"Drush status\" && ./vendor/bin/drush st --fields=site"', static::PATH_DRUPAL, ); $process->run(); $this->assertOutputEquals(<<getOutput()); } @@ -279,7 +282,7 @@ public function testDrushCapitalized(): void { */ public function testWithMixedPlaceholders(): void { $process = Process::fromShellCommandline( - 'drall exec "./vendor/bin/drush --uri=@@dir st && ./vendor/bin/drush @@site.local st"', + 'drall exec --no-progress -- "./vendor/bin/drush --uri=@@dir st && ./vendor/bin/drush @@site.local st"', static::PATH_DRUPAL, ); $process->run(); @@ -294,21 +297,21 @@ public function testWithMixedPlaceholders(): void { */ public function testWithDirPlaceholder(): void { $process = Process::fromShellCommandline( - 'drall exec ls web/sites/@@dir/settings.php', + 'drall exec --no-progress -- ls web/sites/@@dir/settings.php', static::PATH_DRUPAL, ); $process->run(); $this->assertOutputEquals(<<getOutput()); } @@ -318,57 +321,27 @@ public function testWithDirPlaceholder(): void { */ public function testWithFilter(): void { $process1 = Process::fromShellCommandline( - 'drall exec --filter=leo -- ./vendor/bin/drush st --field=site', + 'drall exec --no-progress --filter=leo -- ./vendor/bin/drush st --field=site', static::PATH_DRUPAL, ); $process1->run(); $this->assertOutputEquals(<<getOutput()); // Short form. $process2 = Process::fromShellCommandline( - 'drall exec -f leo -- ./vendor/bin/drush st --field=site', + 'drall exec --no-progress -f leo -- ./vendor/bin/drush st --field=site', static::PATH_DRUPAL, ); $process2->run(); $this->assertOutputEquals(<<getOutput()); - } - - /** - * @testdox With @@dir placeholder and --debug. - */ - public function testWithDirPlaceholderAndDebug(): void { - $process = Process::fromShellCommandline( - 'drall exec --debug -- ls web/sites/@@dir/settings.php', - static::PATH_DRUPAL, - ); - $process->run(); - $this->assertOutputEquals(<<getOutput()); +EOF, $process2->getOutput()); } /** @@ -376,29 +349,29 @@ public function testWithDirPlaceholderAndDebug(): void { */ public function testWithGroup(): void { $process1 = Process::fromShellCommandline( - 'drall exec --group=bluish -- ./vendor/bin/drush st --field=site', + 'drall exec --no-progress --group=bluish -- ./vendor/bin/drush st --field=site', static::PATH_DRUPAL, ); $process1->run(); $this->assertOutputEquals(<<getOutput()); // Short form. $process2 = Process::fromShellCommandline( - 'drall exec -g bluish -- ./vendor/bin/drush st --field=site', + 'drall exec --no-progress -g bluish -- ./vendor/bin/drush st --field=site', static::PATH_DRUPAL, ); $process2->run(); $this->assertOutputEquals(<<getOutput()); } @@ -408,16 +381,16 @@ public function testWithGroup(): void { */ public function testWithGroupEnvVar(): void { $process = Process::fromShellCommandline( - 'drall exec -- ./vendor/bin/drush st --field=site', + 'drall exec --no-progress -- ./vendor/bin/drush st --field=site', static::PATH_DRUPAL, ['DRALL_GROUP' => 'bluish'], ); $process->run(); $this->assertOutputEquals(<<getOutput()); } @@ -427,51 +400,21 @@ public function testWithGroupEnvVar(): void { */ public function testWithSitePlaceholder(): void { $process = Process::fromShellCommandline( - 'drall exec ./vendor/bin/drush -- @@site.local core:status --fields=site', + 'drall exec --no-progress ./vendor/bin/drush -- @@site.local core:status --fields=site', static::PATH_DRUPAL, ); $process->run(); $this->assertOutputEquals(<<getOutput()); - } - - /** - * @testdox With @@site placeholder and --debug. - */ - public function testWithSitePlaceholderDebug(): void { - $process = Process::fromShellCommandline( - 'drall exec --debug -- ./vendor/bin/drush @@site.local st --fields=site', - static::PATH_DRUPAL, - ); - $process->run(); - $this->assertOutputEquals(<<getOutput()); } @@ -481,15 +424,15 @@ public function testWithSitePlaceholderDebug(): void { */ public function testWithSitePlaceholderAndGroup(): void { $process = Process::fromShellCommandline( - 'drall exec --group=bluish -- ./vendor/bin/drush @@site.local st --field=site', + 'drall exec --no-progress --group=bluish -- ./vendor/bin/drush @@site.local st --field=site', static::PATH_DRUPAL, ); $process->run(); $this->assertOutputEquals(<<getOutput()); } @@ -499,7 +442,7 @@ public function testWithSitePlaceholderAndGroup(): void { */ public function testCatchStdErrOutput(): void { $process = Process::fromShellCommandline( - 'drall exec --filter=default -- ./vendor/bin/drush --verbose version', + 'drall exec --no-progress --filter=default -- ./vendor/bin/drush --verbose version', static::PATH_DRUPAL, ); $process->run(); @@ -508,11 +451,11 @@ public function testCatchStdErrOutput(): void { $output = preg_replace('@(Drush version :) ([\d|\.|-]+)@', '$1 x.y.z', $process->getOutput()); $this->assertOutputEquals(<<&1', static::PATH_DRUPAL, - ['DRALL_ENVIRONMENT' => EnvironmentId::Unknown->value], ); $process->run(); $this->assertOutputEquals(<<----------------------] 20%✔ donnie: Done -sites/donnie - 2/5 [===========>----------------] 40%✔ leo: Done -sites/leo - 3/5 [================>-----------] 60%✔ mikey: Done -sites/mikey - 4/5 [======================>-----] 80%✔ ralph: Done -sites/ralph +✔ default: Done + 1/5 [=====>----------------------] 20%sites/donnie +✔ donnie: Done + 2/5 [===========>----------------] 40%sites/leo +✔ leo: Done + 3/5 [================>-----------] 60%sites/mikey +✔ mikey: Done + 4/5 [======================>-----] 80%sites/ralph +✔ ralph: Done 5/5 [============================] 100% - EOF, $process->getOutput()); } /** - * @testdox With --no-progress. + * @testdox With no buffer. */ - public function testWithNoProgressBar(): void { + public function testWithNoBuffer(): void { $process = Process::fromShellCommandline( - 'drall exec --no-progress -- ./vendor/bin/drush st --field=site 2>&1', + 'drall exec --no-progress --no-buffer --verbose -- ./vendor/bin/drush st --field=site 2>&1', static::PATH_DRUPAL, - // The progress bar is always hidden in the "test" environment to avoid - // repeating --no-progress in all commands. Thus, for this test, - // an "unknown" environment is used to check whether --no-progress - // actually works. - ['DRALL_ENVIRONMENT' => EnvironmentId::Unknown->value], ); $process->run(); - $this->assertOutputEquals(<<getOutput()); + $this->assertStringStartsWith( + '[notice] Using no output buffering.' . PHP_EOL, + $process->getOutput(), + ); } /** @@ -577,31 +504,31 @@ public function testWithNoProgressBar(): void { */ public function testWithVerbosityQuiet(): void { $process1 = Process::fromShellCommandline( - 'drall exec --quiet -- ./vendor/bin/drush st --field=site', + 'drall exec --no-progress --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', + 'drall exec --no-progress -q -- ./vendor/bin/drush st --field=site', static::PATH_DRUPAL, ); $process2->run(); $this->assertEquals(<<getOutput()); } @@ -611,64 +538,35 @@ public function testWithVerbosityQuiet(): void { */ public function testWithDryRun(): void { $process1 = Process::fromShellCommandline( - 'drall exec --dry-run -- ./vendor/bin/drush st', + 'drall exec --no-progress --dry-run -- ./vendor/bin/drush st', static::PATH_DRUPAL, ); $process1->run(); $this->assertOutputEquals(<<getOutput()); // Short form. $process2 = Process::fromShellCommandline( - 'drall exec -X -- ./vendor/bin/drush st', + 'drall exec --no-progress -X -- ./vendor/bin/drush st', static::PATH_DRUPAL, ); $process2->run(); $this->assertOutputEquals(<<getOutput()); } - /** - * @testdox With --dry-run --quiet. - */ - public function testWithDryRunQuiet(): void { - $process = Process::fromShellCommandline( - 'drall exec --dry-run --quiet -- ./vendor/bin/drush st', - static::PATH_DRUPAL, - ); - $process->run(); - $this->assertOutputEquals(<<getOutput()); - } - /** * @testdox Shows error when --interval is negative. */ @@ -710,16 +608,28 @@ public function testWithInterval(): void { * @testdox With --workers=2. */ public function testWithWorkers(): void { + // The "default" item, takes 3+ seconds to execute during which the second + // worker processes all other items. Finally, "default" finishes last. $process1 = Process::fromShellCommandline( - 'drall ex --workers=2 --verbose -- drush --uri=@@dir core:status --fields=site', + "drall ex --no-progress --workers=2 --verbose -- \"if [ 'default' = '@@dir' ]; then sleep 3; fi; echo 'Hello @@dir.';\"", static::PATH_DRUPAL, ); $process1->run(); - $this->assertStringStartsWith( - '[notice] Using 2 workers.', - $process1->getOutput(), - ); + $this->assertOutputEquals(<<getOutput()); // Short form. $process2 = Process::fromShellCommandline( @@ -773,20 +683,20 @@ public function testIntervalWithWorkers(): void { */ public function testNonZeroExitCode(): void { $process = Process::fromShellCommandline( - "drall ex -- \"if [ 'default' = '@@dir' ]; then exit 1; fi; echo 'Hello @@dir!';\"", + "drall exec -P -- \"if [ 'default' = '@@dir' ]; then exit 1; fi; echo 'Hello @@dir.';\"", static::PATH_DRUPAL, ); $process->run(); $this->assertOutputEquals(<<getOutput()); $this->assertEquals(1, $process->getExitCode()); @@ -802,7 +712,7 @@ public function testSigInt1(): void { /** * @testdox Two SIGINT gives a forceful exit. */ - public function _testSigInt2(): void { + public function testSigInt2(): void { $this->markTestSkipped('Needs work.'); } diff --git a/test/Unit/Model/EnvironmentIdTest.php b/test/Unit/Model/EnvironmentIdTest.php deleted file mode 100644 index 4480fe3..0000000 --- a/test/Unit/Model/EnvironmentIdTest.php +++ /dev/null @@ -1,19 +0,0 @@ -assertFalse(EnvironmentId::Unknown->isActive()); - $this->assertEquals('test', getenv('DRALL_ENVIRONMENT')); - $this->assertTrue(EnvironmentId::Test->isActive()); - } - -}