diff --git a/.woodpecker/.code_standards_check.yml b/.woodpecker/.code_standards_check.yml index 1217ea3f0b..d147e8fc69 100644 --- a/.woodpecker/.code_standards_check.yml +++ b/.woodpecker/.code_standards_check.yml @@ -43,14 +43,10 @@ steps: - apt-get update -q - DEBIAN_FRONTEND=noninteractive apt-get install -q -y git - if [ ! -z "$${CI_COMMIT_PULL_REQUEST}" ]; then - git fetch --no-tags origin ${CI_COMMIT_TARGET_BRANCH}; - CHANGED_FILES="$(git diff --name-only --diff-filter=ACMRTUXB $(git merge-base FETCH_HEAD origin/${CI_COMMIT_TARGET_BRANCH})..${CI_COMMIT_SHA})"; + git fetch --no-tags --unshallow origin ${CI_COMMIT_TARGET_BRANCH}:refs/remotes/origin/${CI_COMMIT_TARGET_BRANCH}; + CHANGED_FILES="$(git diff --name-only --diff-filter=ACMRTUXB $(git merge-base ${CI_COMMIT_SHA} origin/${CI_COMMIT_TARGET_BRANCH})..${CI_COMMIT_SHA})"; else CHANGED_FILES="$(git diff --name-only --diff-filter=ACMRTUXB ${CI_COMMIT_SHA})"; fi - - if ! echo "$${CHANGED_FILES}" | grep -qE "^(\\.php-cs-fixer(\\.dist)?\\.php|composer\\.lock)$"; then - EXTRA_ARGS=$(printf -- '--path-mode=intersection\n--\n%s' "$${CHANGED_FILES}"); - else - EXTRA_ARGS=''; - fi + - EXTRA_ARGS="--path-mode=intersection -- $${CHANGED_FILES}"; - ./bin/dev/php-cs-fixer/vendor/bin/php-cs-fixer check --config=.php-cs-fixer.dist.php -v --diff --using-cache=no $${EXTRA_ARGS} diff --git a/composer.json b/composer.json index a2c9eea3c9..655f987df9 100644 --- a/composer.json +++ b/composer.json @@ -153,6 +153,7 @@ "dms/phpunit-arraysubset-asserts": "^0.3.1", "mikey179/vfsstream": "^1.6", "mockery/mockery": "^1.3", + "php-mock/php-mock-mockery": "^1.5", "php-mock/php-mock-phpunit": "^2.10", "phpmd/phpmd": "^2.15", "phpstan/phpstan": "^2.0", diff --git a/composer.lock b/composer.lock index e12cc6533c..bae30155dd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b77bf714197f04022a5feb001bf07852", + "content-hash": "32af97f73ec49df2a6cfe98f11bc1d60", "packages": [ { "name": "asika/simple-console", @@ -5441,6 +5441,71 @@ ], "time": "2024-02-10T21:37:25+00:00" }, + { + "name": "php-mock/php-mock-mockery", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/php-mock/php-mock-mockery.git", + "reference": "291994acdc26daf1e3c659cfbe58b01eeb180b7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-mock/php-mock-mockery/zipball/291994acdc26daf1e3c659cfbe58b01eeb180b7f", + "reference": "291994acdc26daf1e3c659cfbe58b01eeb180b7f", + "shasum": "" + }, + "require": { + "mockery/mockery": "^1", + "php": ">=5.6", + "php-mock/php-mock-integration": "^2.2.1 || ^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4|^5|^8" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpmock\\mockery\\": "classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "WTFPL" + ], + "authors": [ + { + "name": "Markus Malkusch", + "email": "markus@malkusch.de", + "homepage": "http://markus.malkusch.de", + "role": "Developer" + } + ], + "description": "Mock built-in PHP functions (e.g. time()) with Mockery. This package relies on PHP's namespace fallback policy. No further extension is needed.", + "homepage": "https://github.com/php-mock/php-mock-mockery", + "keywords": [ + "BDD", + "TDD", + "function", + "mock", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "issues": "https://github.com/php-mock/php-mock-mockery/issues", + "source": "https://github.com/php-mock/php-mock-mockery/tree/1.5.0" + }, + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "time": "2025-03-08T19:46:20+00:00" + }, { "name": "php-mock/php-mock-phpunit", "version": "2.10.0", diff --git a/doc/stats.md b/doc/stats.md new file mode 100644 index 0000000000..1b6a2dfd2a --- /dev/null +++ b/doc/stats.md @@ -0,0 +1,35 @@ +Monitoring +=========== + +* [Home](help) + +## Endpoints + +Currently, there are two endpoints for statistics available + +- `/stats` Returns some basic statistics of the current node +- `/stats/caching` Returns statistics of cache or lock instances, which are used for the currend node + +### `/stats` + +The statistics contain data about the worker performance, the last cron call, number of reports, inbound and outbound packets, posts and comments. + +### `/stats/caching` + +The statistics contain data about the opcache, the used caching (like memory usage, hits/misses, entries, ...) and the used lock (including the cache data) + +## Configuration + +Please define 'stats_key' in your local.config.php in the 'system' section to be able to access the statistics page at /stats?key=your-defined-stats_key + +## 3rd Party monitoring tools + +### Zabbix + +To monitor the health status of your Friendica installation, you can use for example a tool like Zabbix. + +### Prometheus + +To use [prometheus](https://prometheus.io) for gathering metrics, use the [Friendica exporter](https://git.friendi.ca/friendica/friendica-exporter). + +You can find the installation instructions here: https://git.friendi.ca/friendica/friendica-exporter#installation diff --git a/doc/tools.md b/doc/tools.md index 2a273e3650..fac1f4b392 100644 --- a/doc/tools.md +++ b/doc/tools.md @@ -78,15 +78,3 @@ The following will compress */var/log/friendica* (assuming this is the location daily rotate 2 } - -### Zabbix - -To monitor the health status of your Friendica installation, you can use for example a tool like Zabbix. Please define 'stats_key' in your local.config.php in the 'system' section to be able to access the statistics page at /stats?key=your-defined-stats_key - -The statistics contain data about the worker performance, the last cron call, number of reports, inbound and outbound packets, posts and comments. - -### Prometheus - -To use [prometheus](https://prometheus.io) for gathering metrics, use the [Friendica exporter](https://git.friendi.ca/friendica/friendica-exporter). - -You can find the installation instructions here: https://git.friendi.ca/friendica/friendica-exporter#installation diff --git a/src/Core/Cache/Capability/ICanCacheInMemory.php b/src/Core/Cache/Capability/ICanCacheInMemory.php index 82492d368a..550273f5f0 100644 --- a/src/Core/Cache/Capability/ICanCacheInMemory.php +++ b/src/Core/Cache/Capability/ICanCacheInMemory.php @@ -53,4 +53,11 @@ interface ICanCacheInMemory extends ICanCache * @throws CachePersistenceException In case the underlying cache driver has errors during persistence */ public function compareDelete(string $key, $value): bool; + + /** + * Returns some basic statistics of the used Cache instance + * + * @return array Returns an associative array of statistics + */ + public function getStats(): array; } diff --git a/src/Core/Cache/Type/APCuCache.php b/src/Core/Cache/Type/APCuCache.php index f1dde2462c..b269b5aa9d 100644 --- a/src/Core/Cache/Type/APCuCache.php +++ b/src/Core/Cache/Type/APCuCache.php @@ -16,10 +16,9 @@ use Friendica\Core\Cache\Exception\InvalidCacheDriverException; */ class APCuCache extends AbstractCache implements ICanCacheInMemory { - const NAME = 'apcu'; - use CompareSetTrait; use CompareDeleteTrait; + const NAME = 'apcu'; /** * @throws InvalidCacheDriverException @@ -147,4 +146,19 @@ class APCuCache extends AbstractCache implements ICanCacheInMemory return true; } + + /** {@inheritDoc} */ + public function getStats(): array + { + $apcu = apcu_cache_info(); + $sma = apcu_sma_info(); + + return [ + 'entries' => $apcu['num_entries'] ?? null, + 'used_memory' => $apcu['mem_size'] ?? null, + 'hits' => $apcu['num_hits'] ?? null, + 'misses' => $apcu['num_misses'] ?? null, + 'avail_mem' => $sma['avail_mem'] ?? null, + ]; + } } diff --git a/src/Core/Cache/Type/ArrayCache.php b/src/Core/Cache/Type/ArrayCache.php index 7fd44deb0a..148210b4e8 100644 --- a/src/Core/Cache/Type/ArrayCache.php +++ b/src/Core/Cache/Type/ArrayCache.php @@ -15,9 +15,8 @@ use Friendica\Core\Cache\Enum; */ class ArrayCache extends AbstractCache implements ICanCacheInMemory { - const NAME = 'array'; - use CompareDeleteTrait; + const NAME = 'array'; /** @var array Array with the cached data */ protected $cachedData = []; @@ -96,4 +95,10 @@ class ArrayCache extends AbstractCache implements ICanCacheInMemory return false; } } + + /** {@inheritDoc} */ + public function getStats(): array + { + return []; + } } diff --git a/src/Core/Cache/Type/MemcacheCache.php b/src/Core/Cache/Type/MemcacheCache.php index 14bd5e310b..b3a6588841 100644 --- a/src/Core/Cache/Type/MemcacheCache.php +++ b/src/Core/Cache/Type/MemcacheCache.php @@ -19,11 +19,10 @@ use Memcache; */ class MemcacheCache extends AbstractCache implements ICanCacheInMemory { - const NAME = 'memcache'; - use CompareSetTrait; use CompareDeleteTrait; use MemcacheCommandTrait; + const NAME = 'memcache'; /** * @var Memcache @@ -156,4 +155,21 @@ class MemcacheCache extends AbstractCache implements ICanCacheInMemory $cacheKey = $this->getCacheKey($key); return $this->memcache->add($cacheKey, serialize($value), MEMCACHE_COMPRESSED, $ttl); } + + /** {@inheritDoc} */ + public function getStats(): array + { + $stats = $this->memcache->getStats(); + + return [ + 'version' => $stats['version'] ?? null, + 'entries' => $stats['curr_items'] ?? null, + 'used_memory' => $stats['bytes'] ?? null, + 'uptime' => $stats['uptime'] ?? null, + 'connected_clients' => $stats['curr_connections'] ?? null, + 'hits' => $stats['get_hits'] ?? null, + 'misses' => $stats['get_misses'] ?? null, + 'evictions' => $stats['evictions'] ?? null, + ]; + } } diff --git a/src/Core/Cache/Type/MemcachedCache.php b/src/Core/Cache/Type/MemcachedCache.php index 2e970e6078..03ad7d8322 100644 --- a/src/Core/Cache/Type/MemcachedCache.php +++ b/src/Core/Cache/Type/MemcachedCache.php @@ -20,11 +20,10 @@ use Psr\Log\LoggerInterface; */ class MemcachedCache extends AbstractCache implements ICanCacheInMemory { - const NAME = 'memcached'; - use CompareSetTrait; use CompareDeleteTrait; use MemcacheCommandTrait; + const NAME = 'memcached'; /** * @var \Memcached @@ -172,4 +171,27 @@ class MemcachedCache extends AbstractCache implements ICanCacheInMemory $cacheKey = $this->getCacheKey($key); return $this->memcached->add($cacheKey, $value, $ttl); } + + /** {@inheritDoc} */ + public function getStats(): array + { + $stats = $this->memcached->getStats(); + + // get statistics of the first instance + foreach ($stats as $value) { + $stats = $value; + break; + } + + return [ + 'version' => $stats['version'] ?? null, + 'entries' => $stats['curr_items'] ?? null, + 'used_memory' => $stats['bytes'] ?? null, + 'uptime' => $stats['uptime'] ?? null, + 'connected_clients' => $stats['curr_connections'] ?? null, + 'hits' => $stats['get_hits'] ?? null, + 'misses' => $stats['get_misses'] ?? null, + 'evictions' => $stats['evictions'] ?? null, + ]; + } } diff --git a/src/Core/Cache/Type/ProfilerCacheDecorator.php b/src/Core/Cache/Type/ProfilerCacheDecorator.php index 8071b79c5e..113aa76688 100644 --- a/src/Core/Cache/Type/ProfilerCacheDecorator.php +++ b/src/Core/Cache/Type/ProfilerCacheDecorator.php @@ -166,4 +166,14 @@ class ProfilerCacheDecorator implements ICanCache, ICanCacheInMemory { return $this->cache->getName() . ' (with profiler)'; } + + /** {@inheritDoc} */ + public function getStats(): array + { + if ($this->cache instanceof ICanCacheInMemory) { + return $this->cache->getStats(); + } else { + return []; + } + } } diff --git a/src/Core/Cache/Type/RedisCache.php b/src/Core/Cache/Type/RedisCache.php index cf78d362bb..fc04a5433a 100644 --- a/src/Core/Cache/Type/RedisCache.php +++ b/src/Core/Cache/Type/RedisCache.php @@ -207,4 +207,21 @@ class RedisCache extends AbstractCache implements ICanCacheInMemory $this->redis->unwatch(); return false; } + + /** {@inheritDoc} */ + public function getStats(): array + { + $info = $this->redis->info(); + + return [ + 'version' => $info['redis_version'] ?? null, + 'entries' => $this->redis->dbSize() ?? null, + 'used_memory' => $info['used_memory'] ?? null, + 'connected_clients' => $info['connected_clients'] ?? null, + 'uptime' => $info['uptime_in_seconds'] ?? null, + 'hits' => $info['keyspace_hits'] ?? null, + 'misses' => $info['keyspace_misses'] ?? null, + 'evictions' => $info['evicted_keys'] ?? null, + ]; + } } diff --git a/src/Core/Lock/Type/CacheLock.php b/src/Core/Lock/Type/CacheLock.php index c3794d06a7..c7fa75e021 100644 --- a/src/Core/Lock/Type/CacheLock.php +++ b/src/Core/Lock/Type/CacheLock.php @@ -7,7 +7,6 @@ namespace Friendica\Core\Lock\Type; -use Friendica\Core\Cache\Capability\ICanCache; use Friendica\Core\Cache\Capability\ICanCacheInMemory; use Friendica\Core\Cache\Enum\Duration; use Friendica\Core\Cache\Exception\CachePersistenceException; @@ -156,6 +155,16 @@ class CacheLock extends AbstractLock return $success; } + /** + * Returns stats about the cache provider + * + * @return array + */ + public function getCacheStats(): array + { + return $this->cache->getStats(); + } + /** * @param string $key The original key * diff --git a/src/Module/StatsCaching.php b/src/Module/StatsCaching.php new file mode 100644 index 0000000000..668d26e021 --- /dev/null +++ b/src/Module/StatsCaching.php @@ -0,0 +1,108 @@ +config = $config; + $this->cache = $cache; + $this->lock = $lock; + } + + private function isAllowed(array $request): bool + { + return !empty($request['key']) && $request['key'] == $this->config->get('system', 'stats_key'); + } + + /** + * @throws NotFoundException In case the rquest isn't allowed + */ + protected function content(array $request = []): string + { + if (!$this->isAllowed($request)) { + throw new HTTPException\NotFoundException($this->l10n->t('Page not found.')); + } + return ''; + } + + protected function rawContent(array $request = []) + { + if (!$this->isAllowed($request)) { + return; + } + + $data = []; + + // OPcache + if (function_exists('opcache_get_status')) { + $status = opcache_get_status(false); + $data['opcache'] = [ + 'enabled' => $status['opcache_enabled'] ?? false, + 'hit_rate' => $status['opcache_statistics']['opcache_hit_rate'] ?? null, + 'used_memory' => $status['memory_usage']['used_memory'] ?? null, + 'free_memory' => $status['memory_usage']['free_memory'] ?? null, + 'num_cached_scripts' => $status['opcache_statistics']['num_cached_scripts'] ?? null, + ]; + } else { + $data['opcache'] = [ + 'enabled' => false, + ]; + } + + if ($this->cache instanceof ICanCacheInMemory) { + $data['cache'] = [ + 'type' => $this->cache->getName(), + 'stats' => $this->cache->getStats(), + ]; + } else { + $data['cache'] = [ + 'type' => $this->cache->getName(), + ]; + } + + if ($this->lock instanceof CacheLock) { + $data['lock'] = [ + 'type' => $this->lock->getName(), + 'stats' => $this->lock->getCacheStats(), + ]; + } else { + $data['lock'] = [ + 'type' => $this->lock->getName(), + ]; + } + + $this->response->setType('json', 'application/json; charset=utf-8'); + $this->response->addContent(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); + } +} diff --git a/static/routes.config.php b/static/routes.config.php index 88e642c27a..4c7b6949ec 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -642,7 +642,8 @@ return [ ], ], - '/stats' => [Module\Stats::class, [R::GET]], + '/stats' => [Module\Stats::class, [R::GET]], + '/stats/caching' => [Module\StatsCaching::class, [R::GET]], '/network' => [ '[/{content}]' => [Module\Conversation\Network::class, [R::GET]], diff --git a/tests/CacheLockTestCase.php b/tests/CacheLockTestCase.php new file mode 100644 index 0000000000..1599391ece --- /dev/null +++ b/tests/CacheLockTestCase.php @@ -0,0 +1,26 @@ +getCache()->getStats()), array_keys($this->instance->getCacheStats())); + } +} diff --git a/tests/LockTestCase.php b/tests/LockTestCase.php index 9ce86497b7..92abf0ca7b 100644 --- a/tests/LockTestCase.php +++ b/tests/LockTestCase.php @@ -8,21 +8,17 @@ namespace Friendica\Test; use Friendica\Core\Lock\Capability\ICanLock; -use Friendica\Test\MockedTestCase; abstract class LockTestCase extends MockedTestCase { /** - * @var int Start time of the mock (used for time operations) + * Start time of the mock (used for time operations) */ - protected $startTime = 1417011228; + protected int $startTime = 1417011228; + protected ICanLock $instance; - /** - * @var ICanLock - */ - protected $instance; + abstract protected function getInstance(): ICanLock; - abstract protected function getInstance(); protected function setUp(): void { @@ -205,4 +201,6 @@ abstract class LockTestCase extends MockedTestCase self::assertFalse($this->instance->isLocked('wrongLock')); self::assertFalse($this->instance->release('wrongLock')); } + + } diff --git a/tests/src/Core/Cache/APCuCacheTest.php b/tests/src/Core/Cache/APCuCacheTest.php index 117c211b04..47e660b26a 100644 --- a/tests/src/Core/Cache/APCuCacheTest.php +++ b/tests/src/Core/Cache/APCuCacheTest.php @@ -35,4 +35,18 @@ class APCuCacheTest extends MemoryCacheTestCase $this->cache->clear(false); parent::tearDown(); } + + /** + * @small + */ + public function testStats() + { + $stats = $this->instance->getStats(); + + self::assertNotNull($stats['entries']); + self::assertNotNull($stats['used_memory']); + self::assertNotNull($stats['hits']); + self::assertNotNull($stats['misses']); + self::assertNotNull($stats['avail_mem']); + } } diff --git a/tests/src/Core/Cache/ArrayCacheTest.php b/tests/src/Core/Cache/ArrayCacheTest.php index 967cb07bce..50226b0907 100644 --- a/tests/src/Core/Cache/ArrayCacheTest.php +++ b/tests/src/Core/Cache/ArrayCacheTest.php @@ -33,4 +33,12 @@ class ArrayCacheTest extends MemoryCacheTestCase self::markTestSkipped("Array Cache doesn't support TTL"); return true; } + + /** + * @small + */ + public function testGetStats() + { + self::assertEmpty($this->cache->getStats()); + } } diff --git a/tests/src/Core/Cache/MemcacheCacheTest.php b/tests/src/Core/Cache/MemcacheCacheTest.php index abd073f483..c622f22216 100644 --- a/tests/src/Core/Cache/MemcacheCacheTest.php +++ b/tests/src/Core/Cache/MemcacheCacheTest.php @@ -59,4 +59,21 @@ class MemcacheCacheTest extends MemoryCacheTestCase { static::markTestIncomplete('Race condition because of too fast getAllKeys() which uses a workaround'); } + + /** + * @small + */ + public function testStats() + { + $stats = $this->instance->getStats(); + + self::assertNotNull($stats['version']); + self::assertIsNumeric($stats['hits']); + self::assertIsNumeric($stats['misses']); + self::assertIsNumeric($stats['evictions']); + self::assertIsNumeric($stats['entries']); + self::assertIsNumeric($stats['used_memory']); + self::assertGreaterThan(0, $stats['connected_clients']); + self::assertGreaterThan(0, $stats['uptime']); + } } diff --git a/tests/src/Core/Cache/MemcachedCacheTest.php b/tests/src/Core/Cache/MemcachedCacheTest.php index f3b6107b5b..a1c3653f1b 100644 --- a/tests/src/Core/Cache/MemcachedCacheTest.php +++ b/tests/src/Core/Cache/MemcachedCacheTest.php @@ -58,4 +58,21 @@ class MemcachedCacheTest extends MemoryCacheTestCase { static::markTestIncomplete('Race condition because of too fast getAllKeys() which uses a workaround'); } + + /** + * @small + */ + public function testStats() + { + $stats = $this->instance->getStats(); + + self::assertNotNull($stats['version']); + self::assertIsNumeric($stats['hits']); + self::assertIsNumeric($stats['misses']); + self::assertIsNumeric($stats['evictions']); + self::assertIsNumeric($stats['entries']); + self::assertIsNumeric($stats['used_memory']); + self::assertGreaterThan(0, $stats['connected_clients']); + self::assertGreaterThan(0, $stats['uptime']); + } } diff --git a/tests/src/Core/Cache/ProfilerCacheDecoratorTest.php b/tests/src/Core/Cache/ProfilerCacheDecoratorTest.php new file mode 100644 index 0000000000..3f44bcd0bf --- /dev/null +++ b/tests/src/Core/Cache/ProfilerCacheDecoratorTest.php @@ -0,0 +1,56 @@ +shouldReceive('get')->with('system', 'profiler')->once()->andReturn(false); + $config->shouldReceive('get')->with('rendertime', 'callstack')->once()->andReturn(false); + + $this->cache = new ProfilerCacheDecorator(new ArrayCache('localhost'), new Profiler($config)); + return $this->cache; + } + + protected function tearDown(): void + { + $this->cache->clear(false); + parent::tearDown(); + } + + /** + * @doesNotPerformAssertions + */ + public function testTTL() + { + // Array Cache doesn't support TTL + self::markTestSkipped("Array Cache doesn't support TTL"); + return true; + } + + /** + * @small + */ + public function testGetStats() + { + self::assertEmpty($this->cache->getStats()); + } + + public function testGetName() + { + self::assertStringEndsWith(' (with profiler)', $this->instance->getName()); + } +} diff --git a/tests/src/Core/Cache/RedisCacheTest.php b/tests/src/Core/Cache/RedisCacheTest.php index 6169171f40..d16bf5a64c 100644 --- a/tests/src/Core/Cache/RedisCacheTest.php +++ b/tests/src/Core/Cache/RedisCacheTest.php @@ -57,4 +57,21 @@ class RedisCacheTest extends MemoryCacheTestCase $this->cache->clear(false); parent::tearDown(); } + + /** + * @small + */ + public function testStats() + { + $stats = $this->instance->getStats(); + + self::assertNotNull($stats['version']); + self::assertIsNumeric($stats['hits']); + self::assertIsNumeric($stats['misses']); + self::assertIsNumeric($stats['evictions']); + self::assertIsNumeric($stats['entries']); + self::assertIsNumeric($stats['used_memory']); + self::assertGreaterThan(0, $stats['connected_clients']); + self::assertGreaterThan(0, $stats['uptime']); + } } diff --git a/tests/src/Core/Lock/APCuCacheLockTest.php b/tests/src/Core/Lock/APCuCacheLockTest.php index 3ee0d09661..3b6c7904b4 100644 --- a/tests/src/Core/Lock/APCuCacheLockTest.php +++ b/tests/src/Core/Lock/APCuCacheLockTest.php @@ -7,26 +7,39 @@ namespace Friendica\Test\src\Core\Lock; +use Friendica\Core\Cache\Capability\ICanCacheInMemory; use Friendica\Core\Cache\Type\APCuCache; +use Friendica\Core\Lock\Capability\ICanLock; use Friendica\Core\Lock\Type\CacheLock; -use Friendica\Test\LockTestCase; +use Friendica\Test\CacheLockTestCase; /** * @group APCU */ -class APCuCacheLockTest extends LockTestCase +class APCuCacheLockTest extends CacheLockTestCase { + private APCuCache $cache; + private ICanLock $lock; + protected function setUp(): void { if (!APCuCache::isAvailable()) { static::markTestSkipped('APCu is not available'); } + $this->cache = new APCuCache('localhost'); + $this->lock = new CacheLock($this->cache); + parent::setUp(); } - protected function getInstance() + protected function getInstance(): CacheLock { - return new CacheLock(new APCuCache('localhost')); + return $this->lock; + } + + protected function getCache(): ICanCacheInMemory + { + return $this->cache; } } diff --git a/tests/src/Core/Lock/ArrayCacheLockTest.php b/tests/src/Core/Lock/ArrayCacheLockTest.php index 19ac7925c6..07cd88dd1c 100644 --- a/tests/src/Core/Lock/ArrayCacheLockTest.php +++ b/tests/src/Core/Lock/ArrayCacheLockTest.php @@ -7,15 +7,32 @@ namespace Friendica\Test\src\Core\Lock; +use Friendica\Core\Cache\Capability\ICanCacheInMemory; use Friendica\Core\Cache\Type\ArrayCache; use Friendica\Core\Lock\Type\CacheLock; -use Friendica\Test\LockTestCase; +use Friendica\Test\CacheLockTestCase; -class ArrayCacheLockTest extends LockTestCase +class ArrayCacheLockTest extends CacheLockTestCase { - protected function getInstance() + private CacheLock $lock; + private ArrayCache $cache; + + protected function setUp(): void { - return new CacheLock(new ArrayCache('localhost')); + $this->cache = new ArrayCache('localhost'); + $this->lock = new CacheLock($this->cache); + + parent::setUp(); + } + + protected function getInstance(): CacheLock + { + return $this->lock; + } + + protected function getCache(): ICanCacheInMemory + { + return $this->cache; } /** diff --git a/tests/src/Core/Lock/DatabaseLockDriverTest.php b/tests/src/Core/Lock/DatabaseLockDriverTest.php index ebc2b0090f..fbfe61762e 100644 --- a/tests/src/Core/Lock/DatabaseLockDriverTest.php +++ b/tests/src/Core/Lock/DatabaseLockDriverTest.php @@ -7,6 +7,7 @@ namespace Friendica\Test\src\Core\Lock; +use Friendica\Core\Lock\Capability\ICanLock; use Friendica\Core\Lock\Type\DatabaseLock; use Friendica\Test\LockTestCase; use Friendica\Test\Util\CreateDatabaseTrait; @@ -26,7 +27,7 @@ class DatabaseLockDriverTest extends LockTestCase parent::setUp(); } - protected function getInstance() + protected function getInstance(): ICanLock { return new DatabaseLock($this->getDbInstance(), $this->pid); } diff --git a/tests/src/Core/Lock/MemcacheCacheLockTest.php b/tests/src/Core/Lock/MemcacheCacheLockTest.php index 2bb0595cff..8915e6d37c 100644 --- a/tests/src/Core/Lock/MemcacheCacheLockTest.php +++ b/tests/src/Core/Lock/MemcacheCacheLockTest.php @@ -8,19 +8,23 @@ namespace Friendica\Test\src\Core\Lock; use Exception; +use Friendica\Core\Cache\Capability\ICanCacheInMemory; use Friendica\Core\Cache\Type\MemcacheCache; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Lock\Type\CacheLock; -use Friendica\Test\LockTestCase; +use Friendica\Test\CacheLockTestCase; use Mockery; /** * @requires extension Memcache * @group MEMCACHE */ -class MemcacheCacheLockTest extends LockTestCase +class MemcacheCacheLockTest extends CacheLockTestCase { - protected function getInstance() + private CacheLock $lock; + private MemcacheCache $cache; + + protected function setUp(): void { $configMock = Mockery::mock(IManageConfigValues::class); @@ -36,16 +40,24 @@ class MemcacheCacheLockTest extends LockTestCase ->with('system', 'memcache_port') ->andReturn($port); - $lock = null; - try { - $cache = new MemcacheCache($host, $configMock); - $lock = new CacheLock($cache); + $this->cache = new MemcacheCache($host, $configMock); + $this->lock = new CacheLock($this->cache); } catch (Exception $e) { static::markTestSkipped('Memcache is not available'); } - return $lock; + parent::setUp(); + } + + protected function getInstance(): CacheLock + { + return $this->lock; + } + + protected function getCache(): ICanCacheInMemory + { + return $this->cache; } /** diff --git a/tests/src/Core/Lock/MemcachedCacheLockTest.php b/tests/src/Core/Lock/MemcachedCacheLockTest.php index fb38ec3312..522f60c64a 100644 --- a/tests/src/Core/Lock/MemcachedCacheLockTest.php +++ b/tests/src/Core/Lock/MemcachedCacheLockTest.php @@ -8,10 +8,11 @@ namespace Friendica\Test\src\Core\Lock; use Exception; +use Friendica\Core\Cache\Capability\ICanCacheInMemory; use Friendica\Core\Cache\Type\MemcachedCache; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Lock\Type\CacheLock; -use Friendica\Test\LockTestCase; +use Friendica\Test\CacheLockTestCase; use Mockery; use Psr\Log\NullLogger; @@ -19,9 +20,12 @@ use Psr\Log\NullLogger; * @requires extension memcached * @group MEMCACHED */ -class MemcachedCacheLockTest extends LockTestCase +class MemcachedCacheLockTest extends CacheLockTestCase { - protected function getInstance() + private MemcachedCache $cache; + private CacheLock $lock; + + protected function setUp(): void { $configMock = Mockery::mock(IManageConfigValues::class); @@ -35,16 +39,24 @@ class MemcachedCacheLockTest extends LockTestCase $logger = new NullLogger(); - $lock = null; - try { - $cache = new MemcachedCache($host, $configMock, $logger); - $lock = new CacheLock($cache); + $this->cache = new MemcachedCache($host, $configMock, $logger); + $this->lock = new CacheLock($this->cache); } catch (Exception $e) { static::markTestSkipped('Memcached is not available'); } - return $lock; + parent::setUp(); + } + + protected function getInstance(): CacheLock + { + return $this->lock; + } + + protected function getCache(): ICanCacheInMemory + { + return $this->cache; } /** diff --git a/tests/src/Core/Lock/RedisCacheLockTest.php b/tests/src/Core/Lock/RedisCacheLockTest.php index d0237682c3..1136b80c4b 100644 --- a/tests/src/Core/Lock/RedisCacheLockTest.php +++ b/tests/src/Core/Lock/RedisCacheLockTest.php @@ -8,19 +8,20 @@ namespace Friendica\Test\src\Core\Lock; use Exception; +use Friendica\Core\Cache\Capability\ICanCacheInMemory; use Friendica\Core\Cache\Type\RedisCache; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Lock\Type\CacheLock; -use Friendica\Test\LockTestCase; +use Friendica\Test\CacheLockTestCase; use Mockery; /** * @requires extension redis * @group REDIS */ -class RedisCacheLockTest extends LockTestCase +class RedisCacheLockTest extends CacheLockTestCase { - protected function getInstance() + protected function setUp(): void { $configMock = Mockery::mock(IManageConfigValues::class); @@ -45,15 +46,23 @@ class RedisCacheLockTest extends LockTestCase ->with('system', 'redis_password') ->andReturn(null); - $lock = null; - try { - $cache = new RedisCache($host, $configMock); - $lock = new CacheLock($cache); + $this->cache = new RedisCache($host, $configMock); + $this->lock = new CacheLock($this->cache); } catch (Exception $e) { static::markTestSkipped('Redis is not available. Error: ' . $e->getMessage()); } - return $lock; + parent::setUp(); + } + + protected function getInstance(): CAcheLock + { + return $this->lock; + } + + protected function getCache(): ICanCacheInMemory + { + return $this->cache; } } diff --git a/tests/src/Core/Lock/SemaphoreLockTest.php b/tests/src/Core/Lock/SemaphoreLockTest.php index 06b4e02f46..30152ac427 100644 --- a/tests/src/Core/Lock/SemaphoreLockTest.php +++ b/tests/src/Core/Lock/SemaphoreLockTest.php @@ -12,6 +12,7 @@ use Friendica\App; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Config\Model\ReadOnlyFileConfig; use Friendica\Core\Config\ValueObject\Cache; +use Friendica\Core\Lock\Capability\ICanLock; use Friendica\Core\Lock\Type\SemaphoreLock; use Friendica\Core\System; use Friendica\DI; @@ -31,7 +32,7 @@ class SemaphoreLockTest extends LockTestCase $dice->shouldReceive('create')->with(App::class)->andReturn($app); $configCache = new Cache(['system' => ['temppath' => '/tmp']]); - $configMock = new ReadOnlyFileConfig($configCache); + $configMock = new ReadOnlyFileConfig($configCache); $dice->shouldReceive('create')->with(IManageConfigValues::class)->andReturn($configMock); // @todo Because "get_temppath()" is using static methods, we have to initialize the BaseObject @@ -40,7 +41,7 @@ class SemaphoreLockTest extends LockTestCase parent::setUp(); } - protected function getInstance() + protected function getInstance(): ICanLock { return new SemaphoreLock(); } diff --git a/tests/src/Module/StatsCachingTest.php b/tests/src/Module/StatsCachingTest.php new file mode 100644 index 0000000000..58226769a9 --- /dev/null +++ b/tests/src/Module/StatsCachingTest.php @@ -0,0 +1,203 @@ +httpExceptionMock = \Mockery::mock(HTTPException::class); + $this->config = \Mockery::mock(IManageConfigValues::class); + $this->cache = new ArrayCache('localhost'); + $this->lock = new CacheLock($this->cache); + } + + public function testStatsCachingNotAllowed() + { + $this->httpExceptionMock->shouldReceive('content')->andReturn('failed')->once(); + + $response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, [])) + ->run($this->httpExceptionMock); + + self::assertEquals('404', $response->getStatusCode()); + self::assertEquals('Page not found', $response->getReasonPhrase()); + self::assertEquals('failed', $response->getBody()); + } + + public function testStatsCachingWitMinimumCache() + { + $request = [ + 'key' => '12345', + ]; + $this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345'); + PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(false); + + $response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, [])) + ->run($this->httpExceptionMock, $request); + + self::assertJson($response->getBody()); + self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders()); + + $json = json_decode($response->getBody(), true); + + self::assertEquals([ + 'type' => 'array', + 'stats' => [], + ], $json['cache']); + self::assertEquals([ + 'type' => 'array', + 'stats' => [], + ], $json['lock']); + } + + public function testStatsCachingWithDatabase() + { + $request = [ + 'key' => '12345', + ]; + $this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345'); + + $this->cache = new DatabaseCache('localhost', DI::dba()); + $this->lock = new DatabaseLock(DI::dba()); + PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(false); + + $response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, [])) + ->run($this->httpExceptionMock, $request); + + self::assertJson($response->getBody()); + self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders()); + + $json = json_decode($response->getBody(), true); + + self::assertEquals(['enabled' => false], $json['opcache']); + self::assertEquals(['type' => 'database'], $json['cache']); + self::assertEquals(['type' => 'database'], $json['lock']); + } + + public function testStatsCachingWithCache() + { + $request = [ + 'key' => '12345', + ]; + $this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345'); + + $this->cache = new DatabaseCache('localhost', DI::dba()); + $this->lock = new DatabaseLock(DI::dba()); + PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(false); + + $response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, [])) + ->run($this->httpExceptionMock, $request); + + self::assertJson($response->getBody()); + self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders()); + + $json = json_decode($response->getBody(), true); + + self::assertEquals(['enabled' => false], $json['opcache']); + self::assertEquals(['type' => 'database'], $json['cache']); + self::assertEquals(['type' => 'database'], $json['lock']); + } + + public function testStatsCachingWithOpcacheAndNull() + { + $request = [ + 'key' => '12345', + ]; + $this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345'); + + $this->cache = new DatabaseCache('localhost', DI::dba()); + $this->lock = new DatabaseLock(DI::dba()); + PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(true); + PHPMockery::mock("Friendica\\Module", "opcache_get_status")->with(false)->once()->andReturn(false); + + $response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, [])) + ->run($this->httpExceptionMock, $request); + + self::assertJson($response->getBody()); + self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders()); + + $json = json_decode($response->getBody(), true); + + self::assertEquals([ + 'enabled' => false, + 'hit_rate' => null, + 'used_memory' => null, + 'free_memory' => null, + 'num_cached_scripts' => null, + ], $json['opcache']); + self::assertEquals(['type' => 'database'], $json['cache']); + self::assertEquals(['type' => 'database'], $json['lock']); + } + + public function testStatsCachingWithOpcacheAndValues() + { + $request = [ + 'key' => '12345', + ]; + $this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345'); + + $this->cache = new DatabaseCache('localhost', DI::dba()); + $this->lock = new DatabaseLock(DI::dba()); + PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(true); + PHPMockery::mock("Friendica\\Module", "opcache_get_status")->with(false)->once()->andReturn([ + 'opcache_enabled' => true, + 'opcache_statistics' => [ + 'opcache_hit_rate' => 1, + 'num_cached_scripts' => 2, + ], + 'memory_usage' => [ + 'used_memory' => 3, + 'free_memory' => 4, + ] + ]); + + $response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, [])) + ->run($this->httpExceptionMock, $request); + + self::assertJson($response->getBody()); + self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders()); + + $json = json_decode($response->getBody(), true); + + self::assertEquals([ + 'enabled' => true, + 'hit_rate' => 1, + 'used_memory' => 3, + 'free_memory' => 4, + 'num_cached_scripts' => 2, + ], $json['opcache']); + self::assertEquals(['type' => 'database'], $json['cache']); + self::assertEquals(['type' => 'database'], $json['lock']); + } +}