From a795659fc8b3f957c05faaadb1f78b0dffe0a756 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Tue, 6 Sep 2022 16:37:51 +0800 Subject: [PATCH] Clean up of base plugin test case class (#672) - Deprecate "runPluginRefreshCommand" method and replace with "instantiatePlugin", to better describe the functionality within. - Use namespace to guess plugin being detected, falling back to path if there is no namespace given - this fixes testing for symlinked plugins. - Add type hints and return types for most methods --- .../system/tests/bootstrap/PluginTestCase.php | 181 +++++++++--------- 1 file changed, 93 insertions(+), 88 deletions(-) diff --git a/modules/system/tests/bootstrap/PluginTestCase.php b/modules/system/tests/bootstrap/PluginTestCase.php index 4523b0e93..ef424e1a0 100644 --- a/modules/system/tests/bootstrap/PluginTestCase.php +++ b/modules/system/tests/bootstrap/PluginTestCase.php @@ -2,29 +2,38 @@ namespace System\Tests\Bootstrap; -use Backend\Classes\AuthManager; -use System\Classes\UpdateManager; -use System\Classes\PluginManager; -use Winter\Storm\Database\Model as ActiveRecord; -use Backend\Tests\Concerns\InteractsWithAuthentication; -use Config; use Mail; +use Config; use Artisan; -use ReflectionClass; use Exception; +use ReflectionClass; +use Backend\Classes\AuthManager; +use Backend\Tests\Concerns\InteractsWithAuthentication; +use System\Classes\PluginBase; +use System\Classes\PluginManager; +use System\Classes\UpdateManager; +use Winter\Storm\Database\Model as BaseModel; +/** + * Plugin test case. + * + * The base test case that should be used for plugin tests. It instantiates the given plugin and + * its dependencies, and ensures that the plugin is available for use within the tests. + * + * @package winter/wn-system-module + */ abstract class PluginTestCase extends TestCase { use InteractsWithAuthentication; /** - * @var array Cache for storing which plugins have been loaded - * and refreshed. + * @var array Cache for storing which plugins have been loaded. */ protected $pluginTestCaseLoadedPlugins = []; /** * Creates the application. + * * @return \Symfony\Component\HttpKernel\HttpKernelInterface */ public function createApplication() @@ -41,9 +50,7 @@ abstract class PluginTestCase extends TestCase return AuthManager::instance(); }); - /* - * Store database in memory by default unless specified otherwise - */ + // Store database in memory by default unless otherwise specified if (!file_exists(base_path('config/testing/database.php'))) { $app['config']->set('database.connections.testing', [ 'driver' => 'sqlite', @@ -55,9 +62,7 @@ abstract class PluginTestCase extends TestCase // Set random encryption key $app['config']->set('app.key', bin2hex(random_bytes(16))); - /* - * Modify the plugin path away from the test context - */ + // Modify the plugin path away from the test context $app->setPluginsPath(realpath(base_path() . Config::get('cms.pluginsPath'))); return $app; @@ -65,47 +70,35 @@ abstract class PluginTestCase extends TestCase /** * Perform test case set up. - * @return void */ - public function setUp() : void + public function setUp(): void { - /* - * Force reload of Winter singletons - */ + // Reload the plugin and update manager singletons PluginManager::forgetInstance(); UpdateManager::forgetInstance(); - /* - * Create application instance - */ parent::setUp(); - /* - * Ensure system is up to date - */ - $this->runWinterUpCommand(); + // Run all migrations + Artisan::call('winter:up'); - /* - * Detect plugin from test and autoload it - */ + // Reset loaded plugins for this test $this->pluginTestCaseLoadedPlugins = []; - $pluginCode = $this->guessPluginCodeFromTest(); - if ($pluginCode !== false) { - $this->runPluginRefreshCommand($pluginCode, false); + // Detect the plugin being tested and load it automatically + $pluginCode = $this->guessPluginCode(); + if (!is_null($pluginCode)) { + $this->instantiatePlugin($pluginCode, false); } - /* - * Disable mailer - */ + // Disable mailing Mail::pretend(); } /** - * Flush event listeners and collect garbage. - * @return void + * Flush event listeners and tear down. */ - public function tearDown() : void + public function tearDown(): void { $this->flushModelEventListeners(); parent::tearDown(); @@ -113,24 +106,30 @@ abstract class PluginTestCase extends TestCase } /** - * Migrate database using winter:up command. - * @return void - */ - protected function runWinterUpCommand() - { - Artisan::call('winter:up'); - } - - /** - * Since the test environment has loaded all the test plugins - * natively, this method will ensure the desired plugin is - * loaded in the system before proceeding to migrate it. + * Refreshes a plugin for testing. + * + * Since the test environment has loaded all the test plugins natively, this method will ensure + * the desired plugin is loaded in the system before proceeding to migrate it. + * + * @deprecated v1.2.1 Use `instantiatePlugin()` instead. * @return void */ protected function runPluginRefreshCommand($code, $throwException = true) { + $this->instantiatePlugin((string) $code, (bool) $throwException); + } + + /** + * Instantiates a plugin for testing. + * + * @param string $code Plugin code. + * @param boolean $throw Throw an exception if the plugin cannot be found. + */ + protected function instantiatePlugin(string $code, bool $throw = true): void + { + // Check plugin code is valid if (!preg_match('/^[\w+]*\.[\w+]*$/', $code)) { - if (!$throwException) { + if (!$throw) { return; } throw new Exception(sprintf('Invalid plugin code: "%s"', $code)); @@ -138,16 +137,15 @@ abstract class PluginTestCase extends TestCase $manager = PluginManager::instance(); $plugin = $manager->findByIdentifier($code); + $firstLoad = !$plugin; - /* - * First time seeing this plugin, load it up - */ - if (!$plugin) { + // First time seeing this plugin, load it up + if ($firstLoad) { $namespace = '\\'.str_replace('.', '\\', strtolower($code)); $path = array_get($manager->getPluginNamespaces(), $namespace); if (!$path) { - if (!$throwException) { + if (!$throw) { return; } throw new Exception(sprintf('Unable to find plugin with code: "%s"', $code)); @@ -157,49 +155,47 @@ abstract class PluginTestCase extends TestCase $manager->registerPlugin($plugin); } - /* - * Spin over dependencies and refresh them too - */ $this->pluginTestCaseLoadedPlugins[$code] = $plugin; + // Load any dependencies if (!empty($plugin->require)) { foreach ((array) $plugin->require as $dependency) { if (isset($this->pluginTestCaseLoadedPlugins[$dependency])) { continue; } - $this->runPluginRefreshCommand($dependency); + $this->instantiatePlugin($dependency); } } - /* - * Execute the command - */ + // Refresh the plugin's tables Artisan::call('plugin:refresh', ['plugin' => $code, '--force' => true]); + + // Boot the plugin if this is the first load + if ($firstLoad) { + $manager->bootPlugin($plugin); + } } /** * Returns a plugin object from its code, useful for registering events, etc. - * @return PluginBase */ - protected function getPluginObject($code = null) + protected function getPluginObject($code = null): ?PluginBase { - if ($code === null) { - $code = $this->guessPluginCodeFromTest(); - } - - if (isset($this->pluginTestCaseLoadedPlugins[$code])) { - return $this->pluginTestCaseLoadedPlugins[$code]; - } + return $this->pluginTestCaseLoadedPlugins[$code] + ?? $this->pluginTestCaseLoadedPlugins[$this->guessPluginCode()] + ?? null; } /** - * The models in Winter use a static property to store their events, these - * will need to be targeted and reset ready for a new test cycle. + * Flush model event listeners. + * + * The models in Winter use a static property to store their events. These will need to be + * targeted and reset, ready for a new test cycle. + * * Pivot models are an exception since they are internally managed. - * @return void */ - protected function flushModelEventListeners() + protected function flushModelEventListeners(): void { foreach (get_declared_classes() as $class) { if ($class === 'Winter\Storm\Database\Pivot' || strtolower($class) === 'october\rain\database\pivot') { @@ -218,26 +214,35 @@ abstract class PluginTestCase extends TestCase $class::flushEventListeners(); } - ActiveRecord::flushEventListeners(); + BaseModel::flushEventListeners(); } /** - * Locates the plugin code based on the test file location. - * @return string|bool + * Guesses the plugin code being tested. */ - protected function guessPluginCodeFromTest() + protected function guessPluginCode(): ?string { $reflect = new ReflectionClass($this); - $path = $reflect->getFilename(); - $basePath = $this->app->pluginsPath(); + $fqClass = $reflect->getName(); + $namespace = $reflect->getNamespaceName(); - $result = false; + if (empty($namespace)) { + // Try to determine from the path instead + $path = $reflect->getFilename(); + $basePath = $this->app->pluginsPath(); - if (strpos($path, $basePath) === 0) { - $result = ltrim(str_replace('\\', '/', substr($path, strlen($basePath))), '/'); - $result = implode('.', array_slice(explode('/', $result), 0, 2)); + if (!strpos($path, $basePath) === 0) { + return null; + } + + $pluginCode = ltrim(str_replace('\\', '/', substr($path, strlen($basePath))), '/'); + $pluginCode = implode('.', array_slice(explode('/', $pluginCode), 0, 2)); + } else { + // Determine code from namespace + $manager = PluginManager::instance(); + $pluginCode = $manager->getIdentifier($fqClass); } - return $result; + return $pluginCode; } }