mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
e46135be0a
- Turn sake into a symfony/console app - Avoid using HTTPRequest for CLI interaction - Implement abstract hybrid execution path
308 lines
12 KiB
PHP
308 lines
12 KiB
PHP
<?php
|
|
|
|
namespace SilverStripe\Cli\Tests;
|
|
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
use PHPUnit\Framework\Error\Deprecated;
|
|
use ReflectionClass;
|
|
use SilverStripe\Cli\Sake;
|
|
use SilverStripe\Cli\Tests\SakeTest\TestBuildTask;
|
|
use SilverStripe\Cli\Tests\SakeTest\TestCommandLoader;
|
|
use SilverStripe\Cli\Tests\SakeTest\TestConfigCommand;
|
|
use SilverStripe\Cli\Tests\SakeTest\TestConfigPolyCommand;
|
|
use SilverStripe\Core\ClassInfo;
|
|
use SilverStripe\Core\Injector\Injector;
|
|
use SilverStripe\Core\Kernel;
|
|
use SilverStripe\Core\Manifest\VersionProvider;
|
|
use SilverStripe\Dev\BuildTask;
|
|
use SilverStripe\Dev\Deprecation;
|
|
use SilverStripe\Dev\DevelopmentAdmin;
|
|
use SilverStripe\Dev\SapphireTest;
|
|
use SilverStripe\Dev\Tests\DeprecationTest\DeprecationTestException;
|
|
use Symfony\Component\Console\Command\DumpCompletionCommand;
|
|
use Symfony\Component\Console\Input\ArrayInput;
|
|
use Symfony\Component\Console\Output\BufferedOutput;
|
|
|
|
class SakeTest extends SapphireTest
|
|
{
|
|
protected $usesDatabase = false;
|
|
|
|
private $oldErrorHandler = null;
|
|
|
|
public static function provideList(): array
|
|
{
|
|
return [
|
|
'display all' => [
|
|
'addExtra' => true,
|
|
'hideCompletion' => true,
|
|
],
|
|
'display none' => [
|
|
'addExtra' => false,
|
|
'hideCompletion' => false,
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Test adding commands and command loaders to Sake via configuration API
|
|
*/
|
|
#[DataProvider('provideList')]
|
|
public function testList(bool $addExtra, bool $hideCompletion): void
|
|
{
|
|
$sake = new Sake(Injector::inst()->get(Kernel::class));
|
|
$sake->setAutoExit(false);
|
|
$input = new ArrayInput(['list']);
|
|
$input->setInteractive(false);
|
|
$output = new BufferedOutput();
|
|
|
|
if ($addExtra) {
|
|
Sake::config()->merge('commands', [
|
|
TestConfigPolyCommand::class,
|
|
TestConfigCommand::class,
|
|
]);
|
|
Sake::config()->merge('command_loaders', [
|
|
TestCommandLoader::class,
|
|
]);
|
|
}
|
|
Sake::config()->set('hide_completion_command', $hideCompletion);
|
|
// Make sure all tasks are displayed - we'll test hiding them in testHideTasks
|
|
Sake::config()->set('max_tasks_to_display', 0);
|
|
|
|
$sake->run($input, $output);
|
|
|
|
$commandNames = [
|
|
'loader:test-command',
|
|
'test:from-config:standard',
|
|
'test:from-config:poly',
|
|
];
|
|
$commandDescriptions = [
|
|
'command for testing adding custom command loaders',
|
|
'command for testing adding standard commands via config',
|
|
'command for testing adding poly commands via config',
|
|
];
|
|
|
|
$listOutput = $output->fetch();
|
|
|
|
// Check if the extra commands are there or not
|
|
if ($addExtra) {
|
|
foreach ($commandNames as $name) {
|
|
$this->assertStringContainsString($name, $listOutput);
|
|
}
|
|
foreach ($commandDescriptions as $description) {
|
|
$this->assertStringContainsString($description, $listOutput);
|
|
}
|
|
} else {
|
|
foreach ($commandNames as $name) {
|
|
$this->assertStringNotContainsString($name, $listOutput);
|
|
}
|
|
foreach ($commandDescriptions as $description) {
|
|
$this->assertStringNotContainsString($description, $listOutput);
|
|
}
|
|
}
|
|
|
|
// Build task could display automagically as a matter of class inheritance.
|
|
$task = new TestBuildTask();
|
|
$this->assertStringContainsString($task->getName(), $listOutput);
|
|
$this->assertStringContainsString(TestBuildTask::getDescription(), $listOutput);
|
|
|
|
// Check if the completion command is there or not
|
|
$command = new DumpCompletionCommand();
|
|
$completionRegex = "/{$command->getName()}\s+{$command->getDescription()}/";
|
|
if ($hideCompletion) {
|
|
$this->assertDoesNotMatchRegularExpression($completionRegex, $listOutput);
|
|
} else {
|
|
$this->assertMatchesRegularExpression($completionRegex, $listOutput);
|
|
}
|
|
|
|
// Make sure the "help" and "list" commands aren't shown
|
|
$this->assertStringNotContainsString($listOutput, 'List commands', 'the list command should not display');
|
|
$this->assertStringNotContainsString($listOutput, 'Display help for a command', 'the help command should not display');
|
|
}
|
|
|
|
public function testPolyCommandCanRunInCli(): void
|
|
{
|
|
$kernel = Injector::inst()->get(Kernel::class);
|
|
$sake = new Sake($kernel);
|
|
$sake->setAutoExit(false);
|
|
$input = new ArrayInput(['list']);
|
|
$input->setInteractive(false);
|
|
$output = new BufferedOutput();
|
|
|
|
// Add test commands
|
|
Sake::config()->merge('commands', [
|
|
TestConfigPolyCommand::class,
|
|
]);
|
|
|
|
// Disallow these to run in CLI.
|
|
// Note the scenario where all are allowed is in testList().
|
|
TestConfigPolyCommand::config()->set('can_run_in_cli', false);
|
|
TestBuildTask::config()->set('can_run_in_cli', false);
|
|
DevelopmentAdmin::config()->set('allow_all_cli', false);
|
|
|
|
// Must not be in dev mode to test permissions, because all PolyCommand can be run in dev mode.
|
|
$origEnvironment = $kernel->getEnvironment();
|
|
$kernel->setEnvironment('live');
|
|
try {
|
|
$sake->run($input, $output);
|
|
} finally {
|
|
$kernel->setEnvironment($origEnvironment);
|
|
}
|
|
$listOutput = $output->fetch();
|
|
|
|
$allCommands = [
|
|
TestConfigPolyCommand::class,
|
|
TestBuildTask::class,
|
|
];
|
|
foreach ($allCommands as $commandClass) {
|
|
$command = new $commandClass();
|
|
$this->assertStringNotContainsString($command->getName(), $listOutput);
|
|
$this->assertStringNotContainsString($commandClass::getDescription(), $listOutput);
|
|
}
|
|
}
|
|
|
|
public static function provideHideTasks(): array
|
|
{
|
|
return [
|
|
'task count matches limit' => [
|
|
'taskLimit' => 'same',
|
|
'shouldShow' => true,
|
|
],
|
|
'task count lower than limit' => [
|
|
'taskLimit' => 'more',
|
|
'shouldShow' => true,
|
|
],
|
|
'task count greater than limit' => [
|
|
'taskLimit' => 'less',
|
|
'shouldShow' => false,
|
|
],
|
|
'unlimited tasks allowed' => [
|
|
'taskLimit' => 'all',
|
|
'shouldShow' => true,
|
|
],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideHideTasks')]
|
|
public function testHideTasks(string $taskLimit, bool $shouldShow): void
|
|
{
|
|
$sake = new Sake(Injector::inst()->get(Kernel::class));
|
|
$sake->setAutoExit(false);
|
|
$input = new ArrayInput(['list']);
|
|
$input->setInteractive(false);
|
|
$output = new BufferedOutput();
|
|
|
|
// Determine max tasks config value
|
|
$taskInfo = [];
|
|
foreach (ClassInfo::subclassesFor(BuildTask::class, false) as $class) {
|
|
$reflectionClass = new ReflectionClass($class);
|
|
if ($reflectionClass->isAbstract()) {
|
|
continue;
|
|
}
|
|
$singleton = $class::singleton();
|
|
if ($class::canRunInCli() && $singleton->isEnabled()) {
|
|
$taskInfo[$singleton->getName()] = $class::getDescription();
|
|
}
|
|
}
|
|
$maxTasks = match ($taskLimit) {
|
|
'same' => count($taskInfo),
|
|
'more' => count($taskInfo) + 1,
|
|
'less' => count($taskInfo) - 1,
|
|
'all' => 0,
|
|
};
|
|
|
|
Sake::config()->set('max_tasks_to_display', $maxTasks);
|
|
$sake->run($input, $output);
|
|
$listOutput = $output->fetch();
|
|
|
|
// Check the tasks are showing/hidden as appropriate
|
|
if ($shouldShow) {
|
|
foreach ($taskInfo as $name => $description) {
|
|
$this->assertStringContainsString($name, $listOutput);
|
|
$this->assertStringContainsString($description, $listOutput);
|
|
}
|
|
// Shouldn't display the task command
|
|
$this->assertStringNotContainsString('See a list of build tasks to run', $listOutput);
|
|
} else {
|
|
foreach ($taskInfo as $name => $description) {
|
|
$this->assertStringNotContainsString($name, $listOutput);
|
|
$this->assertStringNotContainsString($description, $listOutput);
|
|
}
|
|
// Should display the task command
|
|
$this->assertStringContainsString('See a list of build tasks to run', $listOutput);
|
|
}
|
|
|
|
// Check `sake tasks` ALWAYS shows the tasks
|
|
$input = new ArrayInput(['tasks']);
|
|
$sake->run($input, $output);
|
|
$listOutput = $output->fetch();
|
|
foreach ($taskInfo as $name => $description) {
|
|
$this->assertStringContainsString($name, $listOutput);
|
|
$this->assertStringContainsString($description, $listOutput);
|
|
}
|
|
}
|
|
|
|
public function testVersion(): void
|
|
{
|
|
$sake = new Sake(Injector::inst()->get(Kernel::class));
|
|
$sake->setAutoExit(false);
|
|
$versionProvider = new VersionProvider();
|
|
$this->assertSame($versionProvider->getVersion(), $sake->getVersion());
|
|
}
|
|
|
|
public function testLegacyDevCommands(): void
|
|
{
|
|
$sake = new Sake(Injector::inst()->get(Kernel::class));
|
|
$sake->setAutoExit(false);
|
|
$input = new ArrayInput(['dev/config']);
|
|
$input->setInteractive(false);
|
|
$output = new BufferedOutput();
|
|
|
|
$deprecationsWereEnabled = Deprecation::isEnabled();
|
|
Deprecation::enable();
|
|
$this->expectException(DeprecationTestException::class);
|
|
$expectedErrorString = 'Using the command with the name \'dev/config\' is deprecated. Use \'config:dump\' instead';
|
|
$this->expectExceptionMessage($expectedErrorString);
|
|
|
|
$exitCode = $sake->run($input, $output);
|
|
$this->assertSame(0, $exitCode, 'command should run successfully');
|
|
// $this->assertStringContainsString('abababa', $output->fetch());
|
|
|
|
$this->allowCatchingDeprecations($expectedErrorString);
|
|
try {
|
|
// call outputNotices() directly because the regular shutdown function that emits
|
|
// the notices within Deprecation won't be called until after this unit-test has finished
|
|
Deprecation::outputNotices();
|
|
} finally {
|
|
restore_error_handler();
|
|
$this->oldErrorHandler = null;
|
|
// Disable if they weren't enabled before.
|
|
if (!$deprecationsWereEnabled) {
|
|
Deprecation::disable();
|
|
}
|
|
}
|
|
}
|
|
|
|
private function allowCatchingDeprecations(string $expectedErrorString): void
|
|
{
|
|
// Use custom error handler for two reasons:
|
|
// - Filter out errors for deprecations unrelated to this test class
|
|
// - Allow the use of expectDeprecation(), which doesn't work with E_USER_DEPRECATION by default
|
|
// https://github.com/laminas/laminas-di/pull/30#issuecomment-927585210
|
|
$this->oldErrorHandler = set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use ($expectedErrorString) {
|
|
if ($errno === E_USER_DEPRECATED) {
|
|
if (str_contains($errstr, $expectedErrorString)) {
|
|
throw new DeprecationTestException($errstr);
|
|
} else {
|
|
// Suppress any E_USER_DEPRECATED unrelated to this test class
|
|
return true;
|
|
}
|
|
}
|
|
if (is_callable($this->oldErrorHandler)) {
|
|
return call_user_func($this->oldErrorHandler, $errno, $errstr, $errfile, $errline);
|
|
}
|
|
// Fallback to default PHP error handler
|
|
return false;
|
|
});
|
|
}
|
|
}
|