diff --git a/.horde.yml b/.horde.yml index 4d97082..e0c35c3 100644 --- a/.horde.yml +++ b/.horde.yml @@ -33,13 +33,11 @@ dependencies: horde/vfs: ^3 predis/predis: ^1.1 dev: - composer: - phpunit/phpunit: ^12 - friendsofphp/php-cs-fixer: ^3 - phpstan/phpstan: ^2 + composer: {} autoload-dev: psr-4: - Horde\HashTable\Test\Unit\: test/unit - Horde\HashTable\Test\Memcache\: test/memcache - Horde\HashTable\Test\Redis\: test/redis + Horde\HashTable\Test\Lib\Unit\: test/lib/Unit + Horde\HashTable\Test\Lib\Integration\: test/lib/Integration + Horde\HashTable\Test\Src\Unit\: test/src/Unit + Horde\HashTable\Test\Src\Integration\: test/src/Integration vendor: horde diff --git a/composer.json b/composer.json index 36cccdb..d874fe4 100644 --- a/composer.json +++ b/composer.json @@ -17,11 +17,7 @@ "php": "^7.4 || ^8", "horde/exception": "^3 || dev-FRAMEWORK_6_0" }, - "require-dev": { - "phpunit/phpunit": "^12", - "friendsofphp/php-cs-fixer": "^3", - "phpstan/phpstan": "^2" - }, + "require-dev": {}, "suggest": { "horde/log": "^3 || dev-FRAMEWORK_6_0", "horde/memcache": "^3 || dev-FRAMEWORK_6_0", @@ -38,9 +34,10 @@ }, "autoload-dev": { "psr-4": { - "Horde\\HashTable\\Test\\Unit\\": "test/unit", - "Horde\\HashTable\\Test\\Memcache\\": "test/memcache", - "Horde\\HashTable\\Test\\Redis\\": "test/redis" + "Horde\\HashTable\\Test\\Lib\\Unit\\": "test/lib/Unit", + "Horde\\HashTable\\Test\\Lib\\Integration\\": "test/lib/Integration", + "Horde\\HashTable\\Test\\Src\\Unit\\": "test/src/Unit", + "Horde\\HashTable\\Test\\Src\\Integration\\": "test/src/Integration" } }, "config": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 489b2c1..b8be556 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,17 +14,12 @@ colors="true"> - test/unit test/src/Unit + test/lib/Unit test/src/Integration - - - test/memcache - - - test/redis + test/lib/Integration diff --git a/test/lib/Integration/MemcacheTest.php b/test/lib/Integration/MemcacheTest.php new file mode 100644 index 0000000..bf55d32 --- /dev/null +++ b/test/lib/Integration/MemcacheTest.php @@ -0,0 +1,61 @@ + + * @category Horde + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @package HashTable + * @subpackage UnitTests + */ + +namespace Horde\HashTable\Test\Lib\Integration; + +use Horde\HashTable\Test\Lib\Unit\TestBase; +use Horde_HashTable_Memcache; +use Horde_Memcache; +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\Group; + +/** + * Tests for the legacy HashTable memcache storage driver (lib/). + * + * @author Jan Schneider + * @category Horde + * @copyright 2015-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @package HashTable + * @subpackage UnitTests + */ +#[CoversNothing] +#[Group('integration')] +class MemcacheTest extends TestBase +{ + public static function setUpBeforeClass(): void + { + if ((!extension_loaded('memcache') && !extension_loaded('memcached'))) { + self::$_skip = 'memcache/memcached extension not available.'; + return; + } + + $config = self::getConfig('HASHTABLE_MEMCACHE_TEST_CONFIG', __DIR__ . '/..'); + if (!$config || !isset($config['hashtable']['memcache'])) { + self::$_skip = 'Memcache configuration not available.'; + return; + } + + $memcache = new Horde_Memcache( + array_merge( + $config['hashtable']['memcache'], + ['prefix' => 'horde_hashtable_memcachetest'] + ) + ); + self::$_driver = new Horde_HashTable_Memcache(['memcache' => $memcache]); + } +} diff --git a/test/lib/Integration/PredisTest.php b/test/lib/Integration/PredisTest.php new file mode 100644 index 0000000..e015357 --- /dev/null +++ b/test/lib/Integration/PredisTest.php @@ -0,0 +1,56 @@ + + * @category Horde + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @package HashTable + * @subpackage UnitTests + */ + +namespace Horde\HashTable\Test\Lib\Integration; + +use Horde\HashTable\Test\Lib\Unit\TestBase; +use Horde_HashTable_Predis; +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\Group; +use Predis\Client; + +/** + * Tests for the legacy HashTable Predis storage driver (lib/). + * + * @author Jan Schneider + * @category Horde + * @copyright 2015-2026 Horde LLC + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @package HashTable + * @subpackage UnitTests + */ +#[CoversNothing] +#[Group('integration')] +class PredisTest extends TestBase +{ + public static function setUpBeforeClass(): void + { + if (!class_exists(Client::class)) { + self::$_skip = 'predis/predis not installed'; + return; + } + + $config = self::getConfig('HASHTABLE_PREDIS_TEST_CONFIG', __DIR__ . '/..'); + if (!$config || !isset($config['hashtable']['predis'])) { + self::$_skip = 'Predis configuration not available.'; + return; + } + + $predis = new Client($config['hashtable']['predis']); + self::$_driver = new Horde_HashTable_Predis(['predis' => $predis]); + } +} diff --git a/test/unit/conf.php.dist b/test/lib/Integration/conf.php.dist similarity index 100% rename from test/unit/conf.php.dist rename to test/lib/Integration/conf.php.dist diff --git a/test/unit/MemoryTest.php b/test/lib/Unit/MemoryTest.php similarity index 71% rename from test/unit/MemoryTest.php rename to test/lib/Unit/MemoryTest.php index 7175219..a8a23c8 100644 --- a/test/unit/MemoryTest.php +++ b/test/lib/Unit/MemoryTest.php @@ -1,7 +1,9 @@ * @category Horde - * @copyright 2013-2016 Horde LLC + * @copyright 2013-2026 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package HashTable * @subpackage UnitTests - * @coversNothing */ +#[CoversNothing] class MemoryTest extends TestBase { public static function setUpBeforeClass(): void diff --git a/test/unit/TestBase.php b/test/lib/Unit/TestBase.php similarity index 74% rename from test/unit/TestBase.php rename to test/lib/Unit/TestBase.php index 7ef9b92..4a1fae5 100644 --- a/test/unit/TestBase.php +++ b/test/lib/Unit/TestBase.php @@ -1,30 +1,31 @@ * @category Horde - * @copyright 2013 Horde LLC - * @ignore + * @copyright 2013-2026 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package HashTable * @subpackage UnitTests @@ -42,6 +43,29 @@ public function setUp(): void } } + /** + * Load test configuration from environment variable or conf.php file. + */ + protected static function getConfig(string $env, ?string $path = null): ?array + { + $conf = []; + $config = getenv($env); + if ($config) { + $json = json_decode($config, true); + if ($json) { + return $json; + } + } + + $configFile = ($path ?? __DIR__) . '/conf.php'; + if (file_exists($configFile)) { + require $configFile; + return $conf; + } + + return null; + } + public static function tearDownAfterClass(): void { if (self::$_driver) { @@ -99,5 +123,4 @@ public function testDelete() $this->assertTrue(self::$_driver->delete('foo3')); $this->assertTrue(self::$_driver->delete('foo4')); } - } diff --git a/test/memcache/MemcacheTest.php b/test/memcache/MemcacheTest.php deleted file mode 100644 index d5a9a99..0000000 --- a/test/memcache/MemcacheTest.php +++ /dev/null @@ -1,51 +0,0 @@ - - * @category Horde - * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - * @package HashTable - * @subpackage UnitTests - */ - -namespace Horde\HashTable\Test\Memcache; - -use Horde\HashTable\Test\Unit\TestBase; -use Horde_Memcache; -use Horde_HashTable_Memcache; - -/** - * Tests for the HashTable memcache storage driver. - * - * @author Jan Schneider - * @category Horde - * @copyright 2015-2016 Horde LLC - * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - * @package HashTable - * @subpackage UnitTests - * @coversNothing - */ -class MemcacheTest extends TestBase -{ - public static function setUpBeforeClass(): void - { - if ((extension_loaded('memcache') || extension_loaded('memcached')) - && ($config = self::getConfig('HASHTABLE_MEMCACHE_TEST_CONFIG', __DIR__ . '/..')) - && isset($config['hashtable']['memcache'])) { - $memcache = new Horde_Memcache( - array_merge( - $config['hashtable']['memcache'], - ['prefix' => 'horde_hashtable_memcachetest'] - ) - ); - self::$_driver = new Horde_HashTable_Memcache(['memcache' => $memcache]); - } else { - self::$_skip = 'Memcache or configuration not available.'; - } - } -} diff --git a/test/redis/PredisTest.php b/test/redis/PredisTest.php deleted file mode 100644 index 6ffea97..0000000 --- a/test/redis/PredisTest.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @category Horde - * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - * @package HashTable - * @subpackage UnitTests - */ - -namespace Horde\HashTable\Test\Redis; - -use Horde\HashTable\Test\Unit\TestBase; -use Horde_HashTable_Predis; - -/** - * Tests for the HashTable redis storage driver. - * - * @author Jan Schneider - * @category Horde - * @copyright 2015-2016 Horde LLC - * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 - * @package HashTable - * @subpackage UnitTests - * @coversNothing - */ -class PredisTest extends TestBase -{ - public static function setUpBeforeClass(): void - { - if (class_exists('Predis\Client') - && ($config = self::getConfig('HASHTABLE_PREDIS_TEST_CONFIG', __DIR__ . '/..')) - && isset($config['hashtable']['predis'])) { - $predis = new Predis\Client($config['hashtable']['predis']); - self::$_driver = new Horde_HashTable_Predis(['predis' => $predis]); - } else { - self::$_skip = 'Predis or configuration not available.'; - } - } -} diff --git a/test/src/Integration/MemcacheTest.php b/test/src/Integration/MemcacheTest.php new file mode 100644 index 0000000..a5c8f4e --- /dev/null +++ b/test/src/Integration/MemcacheTest.php @@ -0,0 +1,206 @@ + + * @category Horde + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @package HashTable + */ + +namespace Horde\HashTable\Test\Src\Integration; + +use Horde\HashTable\Driver\Memcache; +use Horde\HashTable\HashTable; +use Horde\HashTable\LockableHashTable; +use Horde\Memcache\MemcacheApi; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +/** + * Integration tests for the Memcache HashTable driver. + * + * Requires a running Memcache server on localhost:11211. + */ +#[CoversClass(Memcache::class)] +#[Group('integration')] +final class MemcacheTest extends TestCase +{ + private static MemcacheApi $memcacheApi; + private Memcache $ht; + + public static function setUpBeforeClass(): void + { + if (!class_exists(MemcacheApi::class)) { + self::markTestSkipped('horde/memcache not installed'); + } + + if (!extension_loaded('memcache') && !extension_loaded('memcached')) { + self::markTestSkipped('memcache or memcached extension not available'); + } + + try { + self::$memcacheApi = new MemcacheApi([ + 'hostspec' => ['localhost'], + 'port' => [11211], + 'prefix' => 'hht_integration_test', + ]); + } catch (\Exception $e) { + self::markTestSkipped('Memcache server not available: ' . $e->getMessage()); + } + } + + protected function setUp(): void + { + $this->ht = new Memcache(self::$memcacheApi, prefix: 'mc_test_'); + $this->ht->clear(); + } + + protected function tearDown(): void + { + $this->ht->clear(); + } + + #[Test] + public function implementsHashTable(): void + { + $this->assertInstanceOf(HashTable::class, $this->ht); + } + + #[Test] + public function implementsLockableHashTable(): void + { + $this->assertInstanceOf(LockableHashTable::class, $this->ht); + } + + #[Test] + public function setAndGetString(): void + { + $this->ht->set('key', 'value'); + $this->assertSame('value', $this->ht->get('key')); + } + + #[Test] + public function setAndGetMixedValues(): void + { + $this->ht->set('int', 42); + $this->ht->set('array', ['a' => 'b']); + + $this->assertSame(42, $this->ht->get('int')); + $this->assertSame(['a' => 'b'], $this->ht->get('array')); + } + + #[Test] + public function getReturnsNullOnMiss(): void + { + $this->assertNull($this->ht->get('ghost')); + } + + #[Test] + public function setOverwritesExistingKey(): void + { + $this->ht->set('key', 'first'); + $this->ht->set('key', 'second'); + $this->assertSame('second', $this->ht->get('key')); + } + + #[Test] + public function ttlExpiresKeys(): void + { + $this->ht->set('expires', 'val', ttl: 1); + $this->assertSame('val', $this->ht->get('expires')); + sleep(2); + $this->assertNull($this->ht->get('expires')); + } + + #[Test] + public function replaceExistingKey(): void + { + $this->ht->set('key', 'old'); + $this->assertTrue($this->ht->replace('key', 'new')); + $this->assertSame('new', $this->ht->get('key')); + } + + #[Test] + public function replaceNonExistingReturnsFalse(): void + { + $this->assertFalse($this->ht->replace('ghost', 'val')); + } + + #[Test] + public function existsReturnsTrueForStoredKey(): void + { + $this->ht->set('key', 'val'); + $this->assertTrue($this->ht->exists('key')); + } + + #[Test] + public function existsReturnsFalseForMissingKey(): void + { + $this->assertFalse($this->ht->exists('ghost')); + } + + #[Test] + public function deleteRemovesKey(): void + { + $this->ht->set('key', 'val'); + $this->ht->delete('key'); + $this->assertFalse($this->ht->exists('key')); + } + + #[Test] + public function deleteAcceptsArrayOfKeys(): void + { + $this->ht->set('a', '1'); + $this->ht->set('b', '2'); + $this->ht->delete(['a', 'b']); + $this->assertFalse($this->ht->exists('a')); + $this->assertFalse($this->ht->exists('b')); + } + + #[Test] + public function getMultipleReturnsMixedResults(): void + { + $this->ht->set('a', 'va'); + $this->ht->set('b', 'vb'); + $result = $this->ht->getMultiple(['a', 'b', 'c']); + $this->assertSame('va', $result['a']); + $this->assertSame('vb', $result['b']); + $this->assertNull($result['c']); + } + + #[Test] + public function existsMultipleReturnsMixedResults(): void + { + $this->ht->set('a', 'val'); + $result = $this->ht->existsMultiple(['a', 'b']); + $this->assertTrue($result['a']); + $this->assertFalse($result['b']); + } + + #[Test] + public function clearRemovesAllKeys(): void + { + $this->ht->set('a', '1'); + $this->ht->set('b', '2'); + $this->ht->clear(); + $this->assertNull($this->ht->get('a')); + $this->assertNull($this->ht->get('b')); + } + + #[Test] + public function lockAndUnlock(): void + { + $this->ht->lock('resource'); + $this->ht->unlock('resource'); + $this->assertTrue(true); + } +}