Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add winter:test command #202

Merged
merged 22 commits into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions modules/system/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ protected function registerConsole()
$this->registerConsoleCommand('winter.passwd', 'System\Console\WinterPasswd');
$this->registerConsoleCommand('winter.version', 'System\Console\WinterVersion');
$this->registerConsoleCommand('winter.manifest', 'System\Console\WinterManifest');
$this->registerConsoleCommand('winter.test', 'System\Console\WinterTest');

$this->registerConsoleCommand('plugin.install', 'System\Console\PluginInstall');
$this->registerConsoleCommand('plugin.remove', 'System\Console\PluginRemove');
Expand Down
254 changes: 254 additions & 0 deletions modules/system/console/WinterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
<?php namespace System\Console;

use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Process\Exception\ProcessSignaledException;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process;
use System\Classes\PluginManager;
use Winter\Storm\Exception\ApplicationException;

/**
* Console command to run tests for plugins or the Winter CMS core.
*
* If a plugin is provided, this command will search for a `phpunit.xml` file inside the plugin's directory and run its tests.
*
* @package winter\wn-system-module
*/
class WinterTest extends Command
{
/**
* @var string The console command name.
*/
protected $name = 'winter:test';

/**
* @var string The console command signature as ignoreValidationErrors causes options not to be registered.
*/
protected $signature = 'winter:test {?--p|plugin=} {?--c|configuration=} {?--o|core}';

/**
* @var string The console command description.
*/
protected $description = 'Run tests for the Winter CMS core or an existing plugin.';

/**
* @var ?string Path to phpunit binary
*/
protected $phpUnitExec = null;

/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();

/**
* Ignore validation errors as option proxying is used by this command
* @see https://github.com/nunomaduro/collision/blob/stable/src/Adapters/Laravel/Commands/TestCommand.php
*/
$this->ignoreValidationErrors();
}

/**
* Execute the console command.
*
* @throws ApplicationException
* @return int|void
*/
public function handle()
{
$arguments = $this->getAdditionalArguments();

if (($config = $this->option('configuration')) && file_exists($config)) {
return $this->execPhpUnit($config, $arguments);
}

$configs = $this->getPhpUnitConfigs();

if ($this->option('core')) {
if (!$configs['core']) {
throw new ApplicationException("Unable to find the core's phpunit.xml file. Try downloading it from GitHub.");
}
$this->info('Running tests for: Winter CMS core');

return $this->execPhpUnit($configs['core'], $arguments);
}

if ($plugin = $this->option('plugin')) {
if (!isset($configs['plugins'][strtolower($plugin)])) {
throw new ApplicationException(sprintf("Unable to find %s\'s phpunit.xml file", $plugin));
}
$this->info('Running tests for plugin: ' . PluginManager::instance()->normalizeIdentifier($plugin));

return $this->execPhpUnit($configs['plugins'][strtolower($plugin)], $arguments);
}

$exitCode = 0;

foreach (['core', 'plugins'] as $type) {
if (is_array($configs[$type])) {
foreach ($configs[$type] as $plugin => $config) {
$this->info('Running tests for plugin: ' . PluginManager::instance()->normalizeIdentifier($plugin));
$exit = $this->execPhpUnit($config, $arguments);
$exitCode = $exitCode === 0 ? $exit : $exitCode;
}
continue;
}

$this->info('Running tests for Winter CMS: ' . $type);
$exit = $this->execPhpUnit($configs[$type], $arguments);
$exitCode = $exitCode === 0 ? $exit : $exitCode;
}

return $exitCode;
}

/**
* Get the console command options.
*/
protected function getOptions(): array
{
return [
['plugin', 'p', InputOption::VALUE_OPTIONAL, 'The name of the plugin. Ex: AuthorName.PluginName', null],
['configuration', 'c', InputOption::VALUE_OPTIONAL, 'The path to a PHPUnit XML config file', null],
['core', 'o', InputOption::VALUE_NONE, 'Run the Winter CMS core tests'],
];
}

/**
* Execute a phpunit test
*
* @param string $config Path to configuration file
* @param array $args Array of params for PHPUnit
* @return int Exit code from process
*/
protected function execPhpUnit(string $config, array $args): int
{
// Find and bind the phpunit executable
if (!$this->phpUnitExec) {
$this->phpUnitExec = (new ExecutableFinder())
->find('phpunit', base_path('vendor/bin/phpunit'), [base_path('vendor')]);
}

$process = new Process(
array_merge([$this->phpUnitExec, '--configuration=' . $config], $args),
dirname($config),
null,
null
);

// Attempt to set tty mode, catch and warn with the exception message if unsupported
try {
$process->setTty(true);
} catch (\Throwable $e) {
$this->warn($e->getMessage());
}

try {
return $process->run(function ($type, $line) {
$this->output->write($line);
});
} catch (ProcessSignaledException $e) {
if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
throw $e;
}

return 1;
}
}

/**
* Find all PHPUnit config files (core, lib, plugins)
*/
protected function getPhpUnitConfigs(): array
{
$configs = [
'core' => $this->getPhpUnitXmlFile(base_path()),
'plugins' => []
];

foreach (PluginManager::instance()->getPlugins() as $plugin) {
if ($path = $this->getPhpUnitXmlFile($plugin->getPluginPath())) {
$configs['plugins'][strtolower($plugin->getPluginIdentifier())] = $path;
}
}

return $configs;
}

/**
* Search for the config file to use.
* Priority order is: phpunit.xml, phpunit.xml.dist
*/
protected function getPhpUnitXmlFile(string $path): ?string
{
// If a phpunit.xml file exists, returns its path
$distFilePath = $path . DIRECTORY_SEPARATOR . 'phpunit.xml';
if (file_exists($distFilePath)) {
return $distFilePath;
}

// Fallback to phpunit.xml.dist file path if it exists
$configFilePath = $path . DIRECTORY_SEPARATOR . 'phpunit.xml.dist';
if (file_exists($configFilePath)) {
return $configFilePath;
}

return null;
}

/**
* Strips out commands arguments and options in order to return arguments/options for PHPUnit.
*/
protected function getAdditionalArguments(): array
{
$arguments = $_SERVER['argv'];

// First two are always "artisan" and "winter:test"
$arguments = array_slice($arguments, 2);

// If nothing to do then just return
if (!count($arguments)) {
return $arguments;
}

// Get the arguments provided by this command
foreach ($this->getOptions() as $argument) {
// For position 0 & 1, pass their names with appropriate dashes
for ($i = 0; $i < 2; $i++) {
$arguments = $this->removeArgument($arguments, str_repeat('-', 2 - $i) . $argument[$i]);
}
}

return $arguments;
}

/**
* Removes flags from argument list and their value if present
*/
protected function removeArgument(array $arguments, string $remove): array
{
// find args that have trailing chars
$key = array_values(preg_grep("/^({$remove}|{$remove}=).*/i", $arguments));
$remove = (isset($key[0])) ? $key[0] : $remove;

// find the position of arguments to remove
if (($position = array_search($remove, $arguments)) === false) {
return $arguments;
}

// remove argument
unset($arguments[$position]);

// if the next item in the array is not a flag, consider it a value of the removed argument
if (isset($arguments[$position + 1]) && substr($arguments[$position + 1], 0, 1) !== '-') {
unset($arguments[$position + 1]);
}

return array_values($arguments);
}
}