From 9028d7d1f550e25b58c69d81d9b78f366f8c2e73 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 27 Nov 2021 17:13:58 -0600 Subject: [PATCH] tests/extensions/shimmy/ - Import. Also, update comments for LifecycleTest. --- tests/extensions/shimmy/info.xml.template | 34 ++ tests/extensions/shimmy/phpunit.xml.dist | 18 + tests/extensions/shimmy/shimmy.civix.php | 309 ++++++++++++++++++ tests/extensions/shimmy/shimmy.php | 108 ++++++ .../phpunit/E2E/Shimmy/LifecycleTest.php | 182 +++++++++++ .../shimmy/tests/phpunit/bootstrap.php | 64 ++++ 6 files changed, 715 insertions(+) create mode 100644 tests/extensions/shimmy/info.xml.template create mode 100644 tests/extensions/shimmy/phpunit.xml.dist create mode 100644 tests/extensions/shimmy/shimmy.civix.php create mode 100644 tests/extensions/shimmy/shimmy.php create mode 100644 tests/extensions/shimmy/tests/phpunit/E2E/Shimmy/LifecycleTest.php create mode 100644 tests/extensions/shimmy/tests/phpunit/bootstrap.php diff --git a/tests/extensions/shimmy/info.xml.template b/tests/extensions/shimmy/info.xml.template new file mode 100644 index 0000000000..910402cc1f --- /dev/null +++ b/tests/extensions/shimmy/info.xml.template @@ -0,0 +1,34 @@ + + + shimmy + FIXME + FIXME + AGPL-3.0 + + CiviCRM LLC + info@civicrm.org + + + http://FIXME + http://FIXME + http://FIXME + http://www.gnu.org/licenses/agpl-3.0.html + + 2021-03-21 + 1.0 + alpha + + mgmt:hidden + + + 5.0 + + This is a new, undeveloped module + + + + + + CRM/Shimmy + + diff --git a/tests/extensions/shimmy/phpunit.xml.dist b/tests/extensions/shimmy/phpunit.xml.dist new file mode 100644 index 0000000000..8321e70acd --- /dev/null +++ b/tests/extensions/shimmy/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + ./tests/phpunit + + + + + ./ + + + + + + + + diff --git a/tests/extensions/shimmy/shimmy.civix.php b/tests/extensions/shimmy/shimmy.civix.php new file mode 100644 index 0000000000..33dce52cbf --- /dev/null +++ b/tests/extensions/shimmy/shimmy.civix.php @@ -0,0 +1,309 @@ +getUrl(self::LONG_NAME), '/'); + } + return CRM_Core_Resources::singleton()->getUrl(self::LONG_NAME, $file); + } + + /** + * Get the path of a resource file (in this extension). + * + * @param string|NULL $file + * Ex: NULL. + * Ex: 'css/foo.css'. + * @return string + * Ex: '/var/www/example.org/sites/default/ext/org.example.foo'. + * Ex: '/var/www/example.org/sites/default/ext/org.example.foo/css/foo.css'. + */ + public static function path($file = NULL) { + // return CRM_Core_Resources::singleton()->getPath(self::LONG_NAME, $file); + return __DIR__ . ($file === NULL ? '' : (DIRECTORY_SEPARATOR . $file)); + } + + /** + * Get the name of a class within this extension. + * + * @param string $suffix + * Ex: 'Page_HelloWorld' or 'Page\\HelloWorld'. + * @return string + * Ex: 'CRM_Foo_Page_HelloWorld'. + */ + public static function findClass($suffix) { + return self::CLASS_PREFIX . '_' . str_replace('\\', '_', $suffix); + } + +} + +use CRM_Shimmy_ExtensionUtil as E; + +function _shimmy_civix_mixin_polyfill() { + if (!class_exists('CRM_Extension_MixInfo')) { + $polyfill = __DIR__ . '/mixin/polyfill.php'; + (require $polyfill)(E::LONG_NAME, E::SHORT_NAME, E::path()); + } +} + +/** + * (Delegated) Implements hook_civicrm_config(). + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_config + */ +function _shimmy_civix_civicrm_config(&$config = NULL) { + static $configured = FALSE; + if ($configured) { + return; + } + $configured = TRUE; + + $template =& CRM_Core_Smarty::singleton(); + + $extRoot = dirname(__FILE__) . DIRECTORY_SEPARATOR; + $extDir = $extRoot . 'templates'; + + if (is_array($template->template_dir)) { + array_unshift($template->template_dir, $extDir); + } + else { + $template->template_dir = [$extDir, $template->template_dir]; + } + + $include_path = $extRoot . PATH_SEPARATOR . get_include_path(); + set_include_path($include_path); + + _shimmy_civix_mixin_polyfill(); +} + +/** + * Implements hook_civicrm_install(). + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_install + */ +function _shimmy_civix_civicrm_install() { + _shimmy_civix_civicrm_config(); + if ($upgrader = _shimmy_civix_upgrader()) { + $upgrader->onInstall(); + } + _shimmy_civix_mixin_polyfill(); +} + +/** + * Implements hook_civicrm_postInstall(). + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_postInstall + */ +function _shimmy_civix_civicrm_postInstall() { + _shimmy_civix_civicrm_config(); + if ($upgrader = _shimmy_civix_upgrader()) { + if (is_callable([$upgrader, 'onPostInstall'])) { + $upgrader->onPostInstall(); + } + } +} + +/** + * Implements hook_civicrm_uninstall(). + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_uninstall + */ +function _shimmy_civix_civicrm_uninstall() { + _shimmy_civix_civicrm_config(); + if ($upgrader = _shimmy_civix_upgrader()) { + $upgrader->onUninstall(); + } +} + +/** + * (Delegated) Implements hook_civicrm_enable(). + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable + */ +function _shimmy_civix_civicrm_enable() { + _shimmy_civix_civicrm_config(); + if ($upgrader = _shimmy_civix_upgrader()) { + if (is_callable([$upgrader, 'onEnable'])) { + $upgrader->onEnable(); + } + _shimmy_civix_mixin_polyfill(); + } +} + +/** + * (Delegated) Implements hook_civicrm_disable(). + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_disable + * @return mixed + */ +function _shimmy_civix_civicrm_disable() { + _shimmy_civix_civicrm_config(); + if ($upgrader = _shimmy_civix_upgrader()) { + if (is_callable([$upgrader, 'onDisable'])) { + $upgrader->onDisable(); + } + } +} + +/** + * (Delegated) Implements hook_civicrm_upgrade(). + * + * @param $op string, the type of operation being performed; 'check' or 'enqueue' + * @param $queue CRM_Queue_Queue, (for 'enqueue') the modifiable list of pending up upgrade tasks + * + * @return mixed + * based on op. for 'check', returns array(boolean) (TRUE if upgrades are pending) + * for 'enqueue', returns void + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_upgrade + */ +function _shimmy_civix_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) { + if ($upgrader = _shimmy_civix_upgrader()) { + return $upgrader->onUpgrade($op, $queue); + } +} + +/** + * @return CRM_Shimmy_Upgrader + */ +function _shimmy_civix_upgrader() { + if (!file_exists(__DIR__ . '/CRM/Shimmy/Upgrader.php')) { + return NULL; + } + else { + return CRM_Shimmy_Upgrader_Base::instance(); + } +} + +/** + * Inserts a navigation menu item at a given place in the hierarchy. + * + * @param array $menu - menu hierarchy + * @param string $path - path to parent of this item, e.g. 'my_extension/submenu' + * 'Mailing', or 'Administer/System Settings' + * @param array $item - the item to insert (parent/child attributes will be + * filled for you) + * + * @return bool + */ +function _shimmy_civix_insert_navigation_menu(&$menu, $path, $item) { + // If we are done going down the path, insert menu + if (empty($path)) { + $menu[] = [ + 'attributes' => array_merge([ + 'label' => CRM_Utils_Array::value('name', $item), + 'active' => 1, + ], $item), + ]; + return TRUE; + } + else { + // Find an recurse into the next level down + $found = FALSE; + $path = explode('/', $path); + $first = array_shift($path); + foreach ($menu as $key => &$entry) { + if ($entry['attributes']['name'] == $first) { + if (!isset($entry['child'])) { + $entry['child'] = []; + } + $found = _shimmy_civix_insert_navigation_menu($entry['child'], implode('/', $path), $item); + } + } + return $found; + } +} + +/** + * (Delegated) Implements hook_civicrm_navigationMenu(). + */ +function _shimmy_civix_navigationMenu(&$nodes) { + if (!is_callable(['CRM_Core_BAO_Navigation', 'fixNavigationMenu'])) { + _shimmy_civix_fixNavigationMenu($nodes); + } +} + +/** + * Given a navigation menu, generate navIDs for any items which are + * missing them. + */ +function _shimmy_civix_fixNavigationMenu(&$nodes) { + $maxNavID = 1; + array_walk_recursive($nodes, function($item, $key) use (&$maxNavID) { + if ($key === 'navID') { + $maxNavID = max($maxNavID, $item); + } + }); + _shimmy_civix_fixNavigationMenuItems($nodes, $maxNavID, NULL); +} + +function _shimmy_civix_fixNavigationMenuItems(&$nodes, &$maxNavID, $parentID) { + $origKeys = array_keys($nodes); + foreach ($origKeys as $origKey) { + if (!isset($nodes[$origKey]['attributes']['parentID']) && $parentID !== NULL) { + $nodes[$origKey]['attributes']['parentID'] = $parentID; + } + // If no navID, then assign navID and fix key. + if (!isset($nodes[$origKey]['attributes']['navID'])) { + $newKey = ++$maxNavID; + $nodes[$origKey]['attributes']['navID'] = $newKey; + $nodes[$newKey] = $nodes[$origKey]; + unset($nodes[$origKey]); + $origKey = $newKey; + } + if (isset($nodes[$origKey]['child']) && is_array($nodes[$origKey]['child'])) { + _shimmy_civix_fixNavigationMenuItems($nodes[$origKey]['child'], $maxNavID, $nodes[$origKey]['attributes']['navID']); + } + } +} + +/** + * (Delegated) Implements hook_civicrm_entityTypes(). + * + * Find any *.entityType.php files, merge their content, and return. + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes + */ +function _shimmy_civix_civicrm_entityTypes(&$entityTypes) { + $entityTypes = array_merge($entityTypes, []); +} diff --git a/tests/extensions/shimmy/shimmy.php b/tests/extensions/shimmy/shimmy.php new file mode 100644 index 0000000000..f600a18ba7 --- /dev/null +++ b/tests/extensions/shimmy/shimmy.php @@ -0,0 +1,108 @@ + E::ts('New subliminal message'), +// 'name' => 'mailing_subliminal_message', +// 'url' => 'civicrm/mailing/subliminal', +// 'permission' => 'access CiviMail', +// 'operator' => 'OR', +// 'separator' => 0, +// )); +// _shimmy_civix_navigationMenu($menu); +//} diff --git a/tests/extensions/shimmy/tests/phpunit/E2E/Shimmy/LifecycleTest.php b/tests/extensions/shimmy/tests/phpunit/E2E/Shimmy/LifecycleTest.php new file mode 100644 index 0000000000..91a0eebceb --- /dev/null +++ b/tests/extensions/shimmy/tests/phpunit/E2E/Shimmy/LifecycleTest.php @@ -0,0 +1,182 @@ +assertNotEquals('UnitTests', getenv('CIVICRM_UF'), 'This is an end-to-end test involving CLI and HTTP. CIVICRM_UF should not be set to UnitTests.'); + + parent::setUp(); + + $this->mixinTests = []; + $mixinTestFiles = (array) glob($this->getPath('/tests/mixin/*Test.php')); + foreach ($mixinTestFiles as $file) { + require_once $file; + $class = '\\Civi\Shimmy\\Mixins\\' . preg_replace(';\.php$;', '', basename($file)); + $this->mixinTests[] = new $class(); + } + } + + /** + * Install and uninstall the extension. Ensure that various mixins+artifacts work correctly. + * + * This interacts with Civi by running many subprocesses (`cv api3` and `cv api4` commands). + * This style of interaction is a better representation of how day-to-day sysadmin works. + */ + public function testLifecycleWithSubprocesses(): void { + $this->runLifecycle($this->createCvWithSubprocesses()); + } + + /** + * Install and uninstall the extension. Ensure that various mixins+artifacts work correctly. + * + * This interacts with Civi by calling local PHP functions (`civicrm_api3(` and `civicrm_api4()`). + * This style of interaction reveals whether the install/uninstall mechanics have data-leaks that + * may cause subtle/buggy interactions during the transitions. + */ + public function testLifecycleWithLocalFunctions(): void { + $this->runLifecycle($this->createCvWithLocalFunctions()); + } + + /** + * @param object $cv + * The `$cv` object is (roughly speaking) a wrapper for calling `cv`. + * It has method bindings for `$cv->api3()` and `$cv->api4()`. + * Different variations of `$cv` may be supplied - they will execute + * `$cv->api3()` and `$cv->api4()` in slightly different ways. + */ + private function runLifecycle($cv): void { + $this->runMethods('testPreConditions', $cv); + + // Clear out anything from previous runs. + $cv->api3('Extension', 'disable', ['key' => 'shimmy']); + $cv->api3('Extension', 'uninstall', ['key' => 'shimmy']); + + // The main show. + $cv->api3('Extension', 'enable', ['key' => 'shimmy']); + $this->runMethods('testInstalled', $cv); + + // This is a duplicate - make sure things still work after an extra run. + $cv->api3('Extension', 'enable', ['key' => 'shimmy']); + $this->runMethods('testInstalled', $cv); + + // OK, how's the cleanup? + $cv->api3('Extension', 'disable', ['key' => 'shimmy']); + $this->runMethods('testDisabled', $cv); + + $cv->api3('Extension', 'uninstall', ['key' => 'shimmy']); + $this->runMethods('testUninstalled', $cv); + } + + protected static function getPath($suffix = ''): string { + return dirname(__DIR__, 4) . $suffix; + } + + protected function runMethods(string $method, ...$args) { + if (empty($this->mixinTests)) { + $this->fail('Cannot run methods. No mixin tests found.'); + } + foreach ($this->mixinTests as $test) { + $test->$method(...$args); + } + } + + protected function createCvWithLocalFunctions() { + return new class { + + public function api3($entity, $action, $params) { + return civicrm_api3($entity, $action, $params); + } + + public function api4($entity, $action, $params): array { + $params = array_merge(['checkPermissions' => FALSE], $params); + return (array) civicrm_api4($entity, $action, $params); + } + + }; + } + + protected function createCvWithSubprocesses() { + return new class { + + public function api3($entity, $action, $params) { + return $this->cv('api3 --in=json ' . escapeshellarg("$entity.$action"), json_encode($params)); + } + + public function api4($entity, $action, $params): array { + $params = array_merge(['checkPermissions' => FALSE], $params); + return $this->cv('api4 --in=json ' . escapeshellarg("$entity.$action"), json_encode($params)); + } + + /** + * Call the "cv" command. + * + * @param string $cmd + * The rest of the command to send. + * @param string|NULL $pipeData + * Optional data to send to `cv` via pipe. + * @param string $decode + * Ex: 'json' or 'phpcode'. + * @return string|array + * Response output (if the command executed normally). + * @throws \RuntimeException + * If the command terminates abnormally. + */ + protected function cv(string $cmd, ?string $pipeData = NULL, string $decode = 'json') { + $cmd = 'cv ' . $cmd; + $descriptorSpec = array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => STDERR); + $oldOutput = getenv('CV_OUTPUT'); + putenv("CV_OUTPUT=json"); + + // Execute `cv` in the original folder. This is a work-around for + // phpunit/codeception, which seem to manipulate PWD. + $cmd = sprintf('cd %s; %s', escapeshellarg(getenv('PWD')), $cmd); + + $process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__); + putenv("CV_OUTPUT=$oldOutput"); + + if ($pipeData !== NULL) { + fwrite($pipes[0], $pipeData); + } + fclose($pipes[0]); + $result = stream_get_contents($pipes[1]); + fclose($pipes[1]); + if (proc_close($process) !== 0) { + throw new RuntimeException("Command failed ($cmd):\n$result"); + } + switch ($decode) { + case 'raw': + return $result; + + case 'phpcode': + // If the last output is /*PHPCODE*/, then we managed to complete execution. + if (substr(trim($result), 0, 12) !== "/*BEGINPHP*/" || substr(trim($result), -10) !== "/*ENDPHP*/") { + throw new \RuntimeException("Command failed ($cmd):\n$result"); + } + return $result; + + case 'json': + return json_decode($result, 1); + + default: + throw new RuntimeException("Bad decoder format ($decode)"); + } + } + + }; + + } + +} diff --git a/tests/extensions/shimmy/tests/phpunit/bootstrap.php b/tests/extensions/shimmy/tests/phpunit/bootstrap.php new file mode 100644 index 0000000000..4da10a185a --- /dev/null +++ b/tests/extensions/shimmy/tests/phpunit/bootstrap.php @@ -0,0 +1,64 @@ +add('CRM_', __DIR__); +$loader->add('Civi\\', __DIR__); +$loader->add('api_', __DIR__); +$loader->add('api\\', __DIR__); +$loader->register(); + +/** + * Call the "cv" command. + * + * @param string $cmd + * The rest of the command to send. + * @param string $decode + * Ex: 'json' or 'phpcode'. + * @return mixed + * Response output (if the command executed normally). + * For 'raw' or 'phpcode', this will be a string. For 'json', it could be any JSON value. + * @throws \RuntimeException + * If the command terminates abnormally. + */ +function cv(string $cmd, string $decode = 'json') { + $cmd = 'cv ' . $cmd; + $descriptorSpec = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => STDERR]; + $oldOutput = getenv('CV_OUTPUT'); + putenv('CV_OUTPUT=json'); + + // Execute `cv` in the original folder. This is a work-around for + // phpunit/codeception, which seem to manipulate PWD. + $cmd = sprintf('cd %s; %s', escapeshellarg(getenv('PWD')), $cmd); + + $process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__); + putenv("CV_OUTPUT=$oldOutput"); + fclose($pipes[0]); + $result = stream_get_contents($pipes[1]); + fclose($pipes[1]); + if (proc_close($process) !== 0) { + throw new RuntimeException("Command failed ($cmd):\n$result"); + } + switch ($decode) { + case 'raw': + return $result; + + case 'phpcode': + // If the last output is /*PHPCODE*/, then we managed to complete execution. + if (substr(trim($result), 0, 12) !== '/*BEGINPHP*/' || substr(trim($result), -10) !== '/*ENDPHP*/') { + throw new \RuntimeException("Command failed ($cmd):\n$result"); + } + return $result; + + case 'json': + return json_decode($result, 1); + + default: + throw new RuntimeException("Bad decoder format ($decode)"); + } +} -- 2.25.1