From 35f2b6744219dfc82ce0613e6115c205fbafc4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Mudr=C3=A1k?= Date: Mon, 5 Oct 2015 02:08:37 +0200 Subject: [PATCH] MDL-49329 admin: Fix the API for getting remote plugin info The previous version of the plugin manager's method get_remote_plugin_info() was suitable for installing missing dependencies only. To make use of for installing new plugins and/or available updates, it must be clear that we are requesting information for the particular plugin version only, not "given or higher" version. --- admin/renderer.php | 4 +- lib/classes/plugin_manager.php | 105 ++++++++++++---- .../fixtures/testable_plugin_manager.php | 67 ++++------- lib/tests/fixtures/testable_update_api.php | 68 ++++++++++- lib/tests/plugin_manager_test.php | 112 ++++++++++++++---- lib/tests/update_api_test.php | 28 ++++- 6 files changed, 279 insertions(+), 105 deletions(-) diff --git a/admin/renderer.php b/admin/renderer.php index 8c23a88aed8..4c38b83b00a 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -1094,9 +1094,9 @@ class core_admin_renderer extends plugin_renderer_base { foreach ($dependencies as $component => $remoteinfo) { if ($remoteinfo === false) { - // The required version is not available. Let us check is there + // The required version is not available. Let us check if there // is at least some version in the plugins directory. - $remoteinfoanyversion = $pluginman->get_remote_plugin_info($component, ANY_VERSION); + $remoteinfoanyversion = $pluginman->get_remote_plugin_info($component, ANY_VERSION, false); if ($remoteinfoanyversion === false) { $unknown[$component] = $component; } else { diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index c1259abf152..eddce7d5692 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -71,8 +71,10 @@ class core_plugin_manager { protected $pluginsinfo = null; /** @var array of raw subplugins information */ protected $subpluginsinfo = null; - /** @var array cache information about availability in the plugins directory */ - protected $remotepluginsinfo = null; + /** @var array cache information about availability in the plugins directory if requesting "at least" version */ + protected $remotepluginsinfoatleast = null; + /** @var array cache information about availability in the plugins directory if requesting exact version */ + protected $remotepluginsinfoexact = null; /** @var array list of installed plugins $name=>$version */ protected $installedplugins = null; /** @var array list of all enabled plugins $name=>$name */ @@ -83,6 +85,8 @@ class core_plugin_manager { protected $plugintypes = null; /** @var \core\update\code_manager code manager to use for plugins code operations */ protected $codemanager = null; + /** @var \core\update\api client instance to use for accessing download.moodle.org/api/ */ + protected $updateapiclient = null; /** * Direct initiation not allowed, use the factory method {@link self::instance()} @@ -119,10 +123,14 @@ class core_plugin_manager { if (static::$singletoninstance) { static::$singletoninstance->pluginsinfo = null; static::$singletoninstance->subpluginsinfo = null; + static::$singletoninstance->remotepluginsinfoatleast = null; + static::$singletoninstance->remotepluginsinfoexact = null; static::$singletoninstance->installedplugins = null; static::$singletoninstance->enabledplugins = null; static::$singletoninstance->presentplugins = null; static::$singletoninstance->plugintypes = null; + static::$singletoninstance->codemanager = null; + static::$singletoninstance->updateapiclient = null; } } $cache = cache::make('core', 'plugin_manager'); @@ -881,7 +889,7 @@ class core_plugin_manager { } if ($reqs->status !== self::REQUIREMENT_STATUS_OK) { - if ($this->is_remote_plugin_available($otherpluginname, $requiredversion)) { + if ($this->is_remote_plugin_available($otherpluginname, $requiredversion, false)) { $reqs->availability = self::REQUIREMENT_AVAILABLE; } else { $reqs->availability = self::REQUIREMENT_UNAVAILABLE; @@ -894,13 +902,17 @@ class core_plugin_manager { /** * Is the given plugin version available in the plugins directory? * - * @param string $component - * @param string|int $requiredversion ANY_VERSION or the version number + * See {@link self::get_remote_plugin_info()} for the full explanation of how the $version + * parameter is interpretted. + * + * @param string $component plugin frankenstyle name + * @param string|int $version ANY_VERSION or the version number + * @param bool $exactmatch false if "given version or higher" is requested * @return boolean */ - public function is_remote_plugin_available($component, $requiredversion) { + public function is_remote_plugin_available($component, $version, $exactmatch) { - $info = $this->get_remote_plugin_info($component, $requiredversion); + $info = $this->get_remote_plugin_info($component, $version, $exactmatch); if (empty($info)) { // There is no available plugin of that name. @@ -916,13 +928,13 @@ class core_plugin_manager { } /** - * Can the given plugin remote plugin be installed via the admin UI? + * Can the given plugin version be installed via the admin UI? * * @param string $component - * @param string|int $requiredversion ANY_VERSION or the version number + * @param int $version version number * @return boolean */ - public function is_remote_plugin_installable($component, $requiredversion) { + public function is_remote_plugin_installable($component, $version) { global $CFG; // Make sure the feature is not disabled. @@ -931,7 +943,7 @@ class core_plugin_manager { } // Make sure we know there is some version available. - if (!$this->is_remote_plugin_available($component, $requiredversion)) { + if (!$this->is_remote_plugin_available($component, $version, true)) { return false; } @@ -941,7 +953,7 @@ class core_plugin_manager { return false; } - $remoteinfo = $this->get_remote_plugin_info($component, $requiredversion); + $remoteinfo = $this->get_remote_plugin_info($component, $version, true); $localinfo = $this->get_plugin_info($component); if ($localinfo) { @@ -963,20 +975,50 @@ class core_plugin_manager { /** * Returns information about a plugin in the plugins directory. * - * See {@link \core\update\api::find_plugin()} for more details. + * This is typically used when checking for available dependencies (in + * which case the $version represents minimal version we need), or + * when installing an available update or a new plugin from the plugins + * directory (in which case the $version is exact version we are + * interested in). The interpretation of the $version is controlled + * by the $exactmatch argument. * - * @param string $component - * @param string|int $requiredversion ANY_VERSION or the version number + * If a plugin with the given component name is found, data about the + * plugin are returned as an object. The ->version property of the object + * contains the information about the particular plugin version that + * matches best the given critera. The ->version property is false if no + * suitable version of the plugin was found (yet the plugin itself is + * known). + * + * See {@link \core\update\api::validate_pluginfo_format()} for the + * returned data structure. + * + * @param string $component plugin frankenstyle name + * @param string|int $version ANY_VERSION or the version number + * @param bool $exactmatch false if "given version or higher" is requested * @return stdClass|bool false or data object */ - public function get_remote_plugin_info($component, $requiredversion) { + public function get_remote_plugin_info($component, $version, $exactmatch) { - if (!isset($this->remotepluginsinfo[$component][$requiredversion])) { - $client = \core\update\api::client(); - $this->remotepluginsinfo[$component][$requiredversion] = $client->find_plugin($component, $requiredversion); + if ($exactmatch and $version == ANY_VERSION) { + throw new coding_exception('Invalid request for exactly any version, it does not make sense.'); } - return $this->remotepluginsinfo[$component][$requiredversion]; + $client = $this->get_update_api_client(); + + if ($exactmatch) { + // Use client's get_plugin_info() method. + if (!isset($this->remotepluginsinfoexact[$component][$version])) { + $this->remotepluginsinfoexact[$component][$version] = $client->get_plugin_info($component, $version); + } + return $this->remotepluginsinfoexact[$component][$version]; + + } else { + // Use client's find_plugin() method. + if (!isset($this->remotepluginsinfoatleast[$component][$version])) { + $this->remotepluginsinfoatleast[$component][$version] = $client->find_plugin($component, $version); + } + return $this->remotepluginsinfoatleast[$component][$version]; + } } /** @@ -1032,14 +1074,15 @@ class core_plugin_manager { } if ($reqinfo->status != self::REQUIREMENT_STATUS_OK) { if ($reqinfo->availability == self::REQUIREMENT_AVAILABLE) { - $remoteinfo = $this->get_remote_plugin_info($reqname, $reqinfo->reqver); + $remoteinfo = $this->get_remote_plugin_info($reqname, $reqinfo->reqver, false); if (empty($dependencies[$reqname])) { $dependencies[$reqname] = $remoteinfo; } else { - // If two local plugins depend on the two different - // versions of the same remote plugin, pick the - // higher version. + // If resolving requirements has led to two different versions of the same + // remote plugin, pick the higher version. This can happen in cases like one + // plugin requiring ANY_VERSION and another plugin requiring specific higher + // version with lower maturity of a remote plugin. if ($remoteinfo->version->version > $dependencies[$reqname]->version->version) { $dependencies[$reqname] = $remoteinfo; } @@ -1760,4 +1803,18 @@ class core_plugin_manager { return $this->codemanager; } + + /** + * Returns a client for https://download.moodle.org/api/ + * + * @return \core\update\api + */ + protected function get_update_api_client() { + + if ($this->updateapiclient === null) { + $this->updateapiclient = \core\update\api::client(); + } + + return $this->updateapiclient; + } } diff --git a/lib/tests/fixtures/testable_plugin_manager.php b/lib/tests/fixtures/testable_plugin_manager.php index 0410f58d99c..1d2b5cdde8a 100644 --- a/lib/tests/fixtures/testable_plugin_manager.php +++ b/lib/tests/fixtures/testable_plugin_manager.php @@ -25,6 +25,8 @@ defined('MOODLE_INTERNAL') || die(); +require_once(__DIR__.'/testable_update_api.php'); + /** * Testable variant of the core_plugin_manager * @@ -36,6 +38,25 @@ class testable_core_plugin_manager extends core_plugin_manager { /** @var testable_core_plugin_manager holds the singleton instance */ protected static $singletoninstance; + /** + * Allows us to inject items directly into the plugins info tree. + * + * Do not forget to call our reset_caches() after using this method to force a new + * singleton instance. + */ + public function inject_testable_plugininfo($type, $name, \core\plugininfo\base $plugininfo) { + $this->pluginsinfo[$type][$name] = $plugininfo; + } + + /** + * Returns testable subclass of the client. + * + * @return \core\update\testable_api + */ + protected function get_update_api_client() { + return \core\update\testable_api::client(); + } + /** * Mockup implementation of loading available updates info. * @@ -71,50 +92,4 @@ class testable_core_plugin_manager extends core_plugin_manager { return null; } - - /** - * Mockup fetching info about a plugin available in the plugins directory. - * - * This testable method does not actually use {@link \core\update\api}. - * Instead, it provides haerd-coded list fictional plugins and their - * versions. - * - * @param string $component - * @param string|int $requiredversion ANY_VERSION or the version number - * @return stdClass|bool false or data object - */ - public function get_remote_plugin_info($component, $requiredversion) { - - $info = false; - - if ($component === 'foo_bar') { - $info = (object)array( - 'name' => 'Foo bar', - 'component' => 'foo_bar', - 'version' => false, - ); - - if ($requiredversion === ANY_VERSION or $requiredversion <= 2015010100) { - $info->version = (object)array( - 'version' => 2015010100 - ); - } - } - - if ($component === 'foo_baz') { - $info = (object)array( - 'name' => 'Foo baz', - 'component' => 'foo_baz', - 'version' => false, - ); - - if ($requiredversion === ANY_VERSION or $requiredversion <= 2015010100) { - $info->version = (object)array( - 'version' => 2015010100 - ); - } - } - - return $info; - } } diff --git a/lib/tests/fixtures/testable_update_api.php b/lib/tests/fixtures/testable_update_api.php index 3250e815f3a..62ef79e12a7 100644 --- a/lib/tests/fixtures/testable_update_api.php +++ b/lib/tests/fixtures/testable_update_api.php @@ -92,8 +92,8 @@ class testable_api extends api { $version2015093000info = (object)array( 'id' => '6765', 'version' => '2015093000', - 'release' => '', - 'maturity' => '50', + 'release' => '1.0', + 'maturity' => '200', 'downloadurl' => 'http://mood.le/plugins/foo_bar/2015093000.zip', 'downloadmd5' => 'd41d8cd98f00b204e9800998ecf8427e', 'vcssystem' => '', @@ -106,6 +106,50 @@ class testable_api extends api { 'version' => '2015041700', 'release' => '2.9' ), + (object)array( + 'version' => '2015110900', + 'release' => '3.0' + ), + ) + ); + + $version2015100400info = (object)array( + 'id' => '6796', + 'version' => '2015100400', + 'release' => '1.1', + 'maturity' => '200', + 'downloadurl' => 'http://mood.le/plugins/foo_bar/2015100400.zip', + 'downloadmd5' => 'd41d8cd98f00b204e9800998ecf8427e', + 'vcssystem' => '', + 'vcssystemother' => '', + 'vcsrepositoryurl' => '', + 'vcsbranch' => '', + 'vcstag' => '', + 'supportedmoodles' => array( + (object)array( + 'version' => '2015110900', + 'release' => '3.0' + ), + ) + ); + + $version2015100500info = (object)array( + 'id' => '6799', + 'version' => '2015100500', + 'release' => '2.0beta', + 'maturity' => '100', + 'downloadurl' => 'http://mood.le/plugins/foo_bar/2015100500.zip', + 'downloadmd5' => 'd41d8cd98f00b204e9800998ecf8427e', + 'vcssystem' => '', + 'vcssystemother' => '', + 'vcsrepositoryurl' => '', + 'vcsbranch' => '', + 'vcstag' => '', + 'supportedmoodles' => array( + (object)array( + 'version' => '2015110900', + 'release' => '3.0' + ), ) ); @@ -121,6 +165,14 @@ class testable_api extends api { $response->data->pluginfo->version = $version2015093000info; } + if (substr($params['plugin'], -11) === '@2015100400') { + $response->data->pluginfo->version = $version2015100400info; + } + + if (substr($params['plugin'], -11) === '@2015100500') { + $response->data->pluginfo->version = $version2015100500info; + } + } else if ($params['plugin'] === 'foo_bar' and isset($params['branch']) and isset($params['minversion'])) { $response->data = $foobarinfo; $response->info = array( @@ -128,8 +180,16 @@ class testable_api extends api { ); $response->status = '200 OK'; - if ($params['minversion'] <= 2015093000) { - $response->data->pluginfo->version = $version2015093000info; + if ($params['minversion'] <= 2015100400) { + // If two stable versions fullfilling the required version are + // available, the /1.3/pluginfo.php API returns the more recent one. + $response->data->pluginfo->version = $version2015100400info; + + } else if ($params['minversion'] <= 2015100500) { + // The /1.3/pluginfo.php API returns versions with lower + // maturity if it is the only way how to fullfil the + // required minimal version. + $response->data->pluginfo->version = $version2015100500info; } } else { diff --git a/lib/tests/plugin_manager_test.php b/lib/tests/plugin_manager_test.php index 4cd33b3c099..7a99108adf6 100644 --- a/lib/tests/plugin_manager_test.php +++ b/lib/tests/plugin_manager_test.php @@ -34,6 +34,12 @@ require_once($CFG->dirroot.'/lib/tests/fixtures/testable_plugininfo_base.php'); */ class core_plugin_manager_testcase extends advanced_testcase { + public function tearDown() { + // The caches of the testable singleton must be reset explicitly. It is + // safer to kill the whole testable singleton at the end of every test. + testable_core_plugin_manager::reset_caches(); + } + public function test_instance() { $pluginman1 = core_plugin_manager::instance(); $this->assertInstanceOf('core_plugin_manager', $pluginman1); @@ -53,6 +59,25 @@ class core_plugin_manager_testcase extends advanced_testcase { testable_core_plugin_manager::reset_caches(); } + /** + * Make sure that the tearDown() really kills the singleton after this test. + */ + public function test_teardown_works_precheck() { + $pluginman = testable_core_plugin_manager::instance(); + $pluginfo = testable_plugininfo_base::fake_plugin_instance('fake', '/dev/null', 'one', '/dev/null/fake', + 'testable_plugininfo_base', $pluginman); + $pluginman->inject_testable_plugininfo('fake', 'one', $pluginfo); + + $this->assertInstanceOf('\core\plugininfo\base', $pluginman->get_plugin_info('fake_one')); + $this->assertNull($pluginman->get_plugin_info('fake_two')); + } + + public function test_teardown_works_postcheck() { + $pluginman = testable_core_plugin_manager::instance(); + $this->assertNull($pluginman->get_plugin_info('fake_one')); + $this->assertNull($pluginman->get_plugin_info('fake_two')); + } + public function test_get_plugin_types() { // Make sure there are no warnings or errors. $types = core_plugin_manager::instance()->get_plugin_types(); @@ -290,17 +315,36 @@ class core_plugin_manager_testcase extends advanced_testcase { $this->assertTrue($pluginman->some_plugins_updatable()); } - public function test_is_remote_plugin_available() { - // See {@link testable_core_plugin_manager::get_remote_plugin_info()}. + public function test_get_remote_plugin_info() { $pluginman = testable_core_plugin_manager::instance(); - $this->assertTrue($pluginman->is_remote_plugin_available('foo_bar', ANY_VERSION)); - $this->assertTrue($pluginman->is_remote_plugin_available('foo_bar', 2015010100)); - $this->assertFalse($pluginman->is_remote_plugin_available('foo_bar', 2016010100)); - $this->assertFalse($pluginman->is_remote_plugin_available('bar_bar', ANY_VERSION)); + + $this->assertFalse($pluginman->get_remote_plugin_info('not_exists', ANY_VERSION, false)); + + $info = $pluginman->get_remote_plugin_info('foo_bar', 2015093000, true); + $this->assertEquals(2015093000, $info->version->version); + + $info = $pluginman->get_remote_plugin_info('foo_bar', 2015093000, false); + $this->assertEquals(2015100400, $info->version->version); + } + + /** + * @expectedException moodle_exception + */ + public function test_get_remote_plugin_info_exception() { + $pluginman = testable_core_plugin_manager::instance(); + // The combination of ANY_VERSION + $exactmatch is illegal. + $pluginman->get_remote_plugin_info('any_thing', ANY_VERSION, true); + } + + public function test_is_remote_plugin_available() { + $pluginman = testable_core_plugin_manager::instance(); + + $this->assertFalse($pluginman->is_remote_plugin_available('not_exists', ANY_VERSION, false)); + $this->assertTrue($pluginman->is_remote_plugin_available('foo_bar', 2013131313, false)); + $this->assertFalse($pluginman->is_remote_plugin_available('foo_bar', 2013131313, true)); } public function test_resolve_requirements() { - $pluginman = testable_core_plugin_manager::instance(); // Prepare a fake pluginfo instance. @@ -340,30 +384,52 @@ class core_plugin_manager_testcase extends advanced_testcase { $this->assertEquals($pluginman::REQUIREMENT_STATUS_OK, $reqs['core']->status); // Test plugin dependencies and their availability. - // See {@link testable_core_plugin_manager::get_remote_plugin_info()}. - $pluginfo->dependencies = array( - 'foo_bar' => ANY_VERSION, - 'foo_baz' => 2014010100, - 'foo_crazy' => ANY_VERSION, - ); + // See {@link \core\update\testable_api} class. + + $pluginfo->dependencies = array('foo_bar' => ANY_VERSION, 'not_exists' => ANY_VERSION); $reqs = $pluginman->resolve_requirements($pluginfo, 2015110900, 30); $this->assertNull($reqs['foo_bar']->hasver); $this->assertEquals(ANY_VERSION, $reqs['foo_bar']->reqver); $this->assertEquals($pluginman::REQUIREMENT_STATUS_MISSING, $reqs['foo_bar']->status); $this->assertEquals($pluginman::REQUIREMENT_AVAILABLE, $reqs['foo_bar']->availability); - $this->assertEquals($pluginman::REQUIREMENT_AVAILABLE, $reqs['foo_baz']->availability); - $this->assertEquals($pluginman::REQUIREMENT_UNAVAILABLE, $reqs['foo_crazy']->availability); + $this->assertEquals($pluginman::REQUIREMENT_UNAVAILABLE, $reqs['not_exists']->availability); - $pluginfo->dependencies = array( - 'foo_baz' => 2015010100, - ); + $pluginfo->dependencies = array('foo_bar' => 2013122400); $reqs = $pluginman->resolve_requirements($pluginfo, 2015110900, 30); - $this->assertEquals($pluginman::REQUIREMENT_AVAILABLE, $reqs['foo_baz']->availability); + $this->assertEquals($pluginman::REQUIREMENT_AVAILABLE, $reqs['foo_bar']->availability); - $pluginfo->dependencies = array( - 'foo_baz' => 2015010101, - ); + $pluginfo->dependencies = array('foo_bar' => 2015093000); $reqs = $pluginman->resolve_requirements($pluginfo, 2015110900, 30); - $this->assertEquals($pluginman::REQUIREMENT_UNAVAILABLE, $reqs['foo_baz']->availability); + $this->assertEquals($pluginman::REQUIREMENT_AVAILABLE, $reqs['foo_bar']->availability); + + $pluginfo->dependencies = array('foo_bar' => 2015100500); + $reqs = $pluginman->resolve_requirements($pluginfo, 2015110900, 30); + $this->assertEquals($pluginman::REQUIREMENT_AVAILABLE, $reqs['foo_bar']->availability); + + $pluginfo->dependencies = array('foo_bar' => 2025010100); + $reqs = $pluginman->resolve_requirements($pluginfo, 2015110900, 30); + $this->assertEquals($pluginman::REQUIREMENT_UNAVAILABLE, $reqs['foo_bar']->availability); + } + + public function test_missing_dependencies() { + $pluginman = testable_core_plugin_manager::instance(); + + $one = testable_plugininfo_base::fake_plugin_instance('fake', '/dev/null', 'one', '/dev/null/fake', + 'testable_plugininfo_base', $pluginman); + $two = testable_plugininfo_base::fake_plugin_instance('fake', '/dev/null', 'two', '/dev/null/fake', + 'testable_plugininfo_base', $pluginman); + + $pluginman->inject_testable_plugininfo('fake', 'one', $one); + $pluginman->inject_testable_plugininfo('fake', 'two', $two); + + $this->assertEmpty($pluginman->missing_dependencies()); + + $one->dependencies = array('foo_bar' => ANY_VERSION); + $misdeps = $pluginman->missing_dependencies(); + $this->assertEquals(2015100400, $misdeps['foo_bar']->version->version); + + $two->dependencies = array('foo_bar' => 2015100500); + $misdeps = $pluginman->missing_dependencies(); + $this->assertEquals(2015100500, $misdeps['foo_bar']->version->version); } } diff --git a/lib/tests/update_api_test.php b/lib/tests/update_api_test.php index 2a583a7c965..f6661dfd559 100644 --- a/lib/tests/update_api_test.php +++ b/lib/tests/update_api_test.php @@ -31,6 +31,11 @@ require_once(__DIR__.'/fixtures/testable_update_api.php'); /** * Tests for \core\update\api client. * + * Please note many of these tests heavily depend on the behaviour of the + * testable_api client. It is important to make sure that the behaviour of the + * testable_api client perfectly matches the actual behaviour of the live + * services on the given API version. + * * @copyright 2015 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -66,8 +71,12 @@ class core_update_api_testcase extends advanced_testcase { $this->assertFalse($info->version); // Both plugin and the version are available. - $info = $client->get_plugin_info('foo_bar', 2015093000); - $this->assertNotNull($info->version->downloadurl); + foreach (array(2015093000 => MATURITY_STABLE, 2015100400 => MATURITY_STABLE, + 2015100500 => MATURITY_BETA) as $version => $maturity) { + $info = $client->get_plugin_info('foo_bar', $version); + $this->assertNotEmpty($info->version); + $this->assertEquals($maturity, $info->version->maturity); + } } /** @@ -81,15 +90,22 @@ class core_update_api_testcase extends advanced_testcase { $this->assertFalse($client->find_plugin('non_existing')); // The plugin is known but there is no sufficient version. - $info = $client->find_plugin('foo_bar', 2015093001); + $info = $client->find_plugin('foo_bar', 2016010100); $this->assertFalse($info->version); - // Both plugin and the version are available. + // Both plugin and the version are available. Of the two available + // stable versions, the more recent one is returned. $info = $client->find_plugin('foo_bar', 2015093000); - $this->assertNotNull($info->version->downloadurl); + $this->assertEquals(2015100400, $info->version->version); + // If any version is required, the most recent most mature one is + // returned. $info = $client->find_plugin('foo_bar', ANY_VERSION); - $this->assertNotNull($info->version->downloadurl); + $this->assertEquals(2015100400, $info->version->version); + + // Less matured versions are returned if needed. + $info = $client->find_plugin('foo_bar', 2015100500); + $this->assertEquals(2015100500, $info->version->version); } /**