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);
+ }
+}