diff --git a/composer.json b/composer.json index 67f7445..1210e9f 100644 --- a/composer.json +++ b/composer.json @@ -3,13 +3,14 @@ "type": "package", "license": "Elastic-2.0", "require": { - "php": "8.4.*", "filament/filament": "^4.11.6", "internachi/modular": "^2.0 || ^3.0", "internachi/modularize": "^1.1", "laravel/framework": "^11.0|^12.0", "laravel/pennant": "^1.0", "laravel/prompts": "^0.3", + "php": "8.4.*", + "spatie/laravel-health": "^1.40", "tapp/filament-timezone-field": "^3.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 9f54f94..a9f9b80 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": "7659002cb003c462ed5b15db0ac2654a", + "content-hash": "785caa9f7c3cfaa4c652579901993fef", "packages": [ { "name": "blade-ui-kit/blade-heroicons", @@ -6033,6 +6033,82 @@ }, "time": "2023-09-03T09:24:00+00:00" }, + { + "name": "spatie/enum", + "version": "3.13.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/enum.git", + "reference": "f1a0f464ba909491a53e60a955ce84ad7cd93a2c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/enum/zipball/f1a0f464ba909491a53e60a955ce84ad7cd93a2c", + "reference": "f1a0f464ba909491a53e60a955ce84ad7cd93a2c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^8.0" + }, + "require-dev": { + "fakerphp/faker": "^1.9.1", + "larapack/dd": "^1.1", + "phpunit/phpunit": "^9.0", + "vimeo/psalm": "^4.3" + }, + "suggest": { + "fakerphp/faker": "To use the enum faker provider", + "phpunit/phpunit": "To use the enum assertions" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Enum\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brent Roose", + "email": "brent@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Tom Witkowski", + "email": "dev@gummibeer.de", + "homepage": "https://gummibeer.de", + "role": "Developer" + } + ], + "description": "PHP Enums", + "homepage": "https://github.com/spatie/enum", + "keywords": [ + "enum", + "enumerable", + "spatie" + ], + "support": { + "docs": "https://docs.spatie.be/enum", + "issues": "https://github.com/spatie/enum/issues", + "source": "https://github.com/spatie/enum" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2022-04-22T08:51:55+00:00" + }, { "name": "spatie/invade", "version": "2.1.0", @@ -6092,6 +6168,99 @@ ], "time": "2024-05-17T09:06:10+00:00" }, + { + "name": "spatie/laravel-health", + "version": "1.40.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-health.git", + "reference": "6fa735589c04fd99970b434fc22dddbdc716a264" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-health/zipball/6fa735589c04fd99970b434fc22dddbdc716a264", + "reference": "6fa735589c04fd99970b434fc22dddbdc716a264", + "shasum": "" + }, + "require": { + "dragonmantank/cron-expression": "^3.3.1", + "guzzlehttp/guzzle": "^6.5|^7.4.5|^7.2", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/notifications": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "laravel/serializable-closure": "^1.3|^2.0", + "nunomaduro/termwind": "^1.0|^2.0", + "php": "^8.2", + "spatie/enum": "^3.13", + "spatie/laravel-package-tools": "^1.12.1", + "spatie/regex": "^3.1.1|^3.1", + "symfony/process": "^5.4|^6.0|^7.0|^8.0" + }, + "require-dev": { + "laravel/horizon": "^5.9.10", + "nunomaduro/collision": "^5.10|^6.2.1|^6.1|^8.0", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "pestphp/pest": "^2.34|^3.0|^4.0", + "pestphp/pest-plugin-laravel": "^2.3|^3.0|^4.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", + "phpstan/phpstan-phpunit": "^1.1.1|^2.0", + "phpunit/phpunit": "^9.5.21|^9.5.10|^10.5|^11.0|^12.0", + "spatie/laravel-ray": "^1.30", + "spatie/pest-plugin-snapshots": "^1.1|^2.3|^3.0", + "spatie/pest-plugin-test-time": "^1.1.1|^1.1|^2.0|^3.0", + "spatie/temporary-directory": "^2.2", + "spatie/test-time": "^1.3|^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Health": "Spatie\\Health\\Facades\\Health" + }, + "providers": [ + "Spatie\\Health\\HealthServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\Health\\": "src", + "Spatie\\Health\\Database\\Factories\\": "database/factories" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Monitor the health of a Laravel application", + "homepage": "https://github.com/spatie/laravel-health", + "keywords": [ + "laravel", + "laravel-health", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-health/issues", + "source": "https://github.com/spatie/laravel-health/tree/1.40.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-05-28T08:55:40+00:00" + }, { "name": "spatie/laravel-package-tools", "version": "1.93.1", @@ -6153,6 +6322,69 @@ ], "time": "2026-05-19T14:06:37+00:00" }, + { + "name": "spatie/regex", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/regex.git", + "reference": "d543de2019a0068e7b80da0ba24f1c51c7469303" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/regex/zipball/d543de2019a0068e7b80da0ba24f1c51c7469303", + "reference": "d543de2019a0068e7b80da0ba24f1c51c7469303", + "shasum": "" + }, + "require": { + "php": "^8.0|^8.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Regex\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sebastian De Deyne", + "email": "sebastian@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "A sane interface for php's built in preg_* functions", + "homepage": "https://github.com/spatie/regex", + "keywords": [ + "expression", + "expressions", + "regex", + "regular", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/regex/issues", + "source": "https://github.com/spatie/regex/tree/3.1.1" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2021-11-30T21:13:59+00:00" + }, { "name": "spatie/shiki-php", "version": "2.4.0", @@ -14703,5 +14935,5 @@ "php": "8.4.*" }, "platform-dev": {}, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Health/Checks/OpcacheCachedFilesCheck.php b/src/Health/Checks/OpcacheCachedFilesCheck.php new file mode 100644 index 0000000..81bf3f5 --- /dev/null +++ b/src/Health/Checks/OpcacheCachedFilesCheck.php @@ -0,0 +1,82 @@ + + + Copyright © 2016-2026, Canyon GBS LLC. All rights reserved. + + Canyon GBS Common is licensed under the Elastic License 2.0. For more details, + see https://github.com/canyongbs/common/blob/main/LICENSE. + + Notice: + + - You may not provide the software to third parties as a hosted or managed + service, where the service provides users with access to any substantial set of + the features or functionality of the software. + - You may not move, change, disable, or circumvent the license key functionality + in the software, and you may not remove or obscure any functionality in the + software that is protected by the license key. + - You may not alter, remove, or obscure any licensing, copyright, or other notices + of the licensor in the software. Any use of the licensor’s trademarks is subject + to applicable law. + - Canyon GBS LLC respects the intellectual property rights of others and expects the + same in return. Canyon GBS™ and Canyon GBS Common are registered trademarks of + Canyon GBS LLC, and we are committed to enforcing and protecting our trademarks + vigorously. + - The software solution, including services, infrastructure, and code, is offered as a + Software as a Service (SaaS) by Canyon GBS LLC. + - Use of this software implies agreement to the license terms and conditions as stated + in the Elastic License 2.0. + + For more information or inquiries please visit our website at + https://www.canyongbs.com or contact us via email at legal@canyongbs.com. + + +*/ + +namespace CanyonGBS\Common\Health\Checks; + +use CanyonGBS\Common\Health\Services\OpcacheStatusService; +use Spatie\Health\Checks\Check; +use Spatie\Health\Checks\Result; + +class OpcacheCachedFilesCheck extends Check +{ + public function run(): Result + { + $status = app(OpcacheStatusService::class)->getStatus(); + + if ($status === false) { + return Result::make() + ->failed('OPcache is not available.'); + } + + $numCachedScripts = $status['opcache_statistics']['num_cached_scripts'] ?? 0; + $maxCachedKeys = $status['opcache_statistics']['max_cached_keys'] ?? 0; + + if ($maxCachedKeys === 0) { + return Result::make() + ->failed('OPcache max_cached_keys is zero.'); + } + + $usagePercentage = round($numCachedScripts / $maxCachedKeys * 100, 2); + + $result = Result::make() + ->shortSummary("{$numCachedScripts}/{$maxCachedKeys}") + ->meta([ + 'num_cached_scripts' => $numCachedScripts, + 'max_cached_keys' => $maxCachedKeys, + 'usage_percentage' => $usagePercentage, + ]); + + if ($numCachedScripts >= $maxCachedKeys) { + return $result->failed("OPcache file cache is full ({$numCachedScripts}/{$maxCachedKeys})"); + } + + if ($usagePercentage >= 90) { + return $result->warning("OPcache file cache is nearly full ({$usagePercentage}% used)"); + } + + return $result->ok(); + } +} diff --git a/src/Health/Checks/OpcacheHitRateCheck.php b/src/Health/Checks/OpcacheHitRateCheck.php new file mode 100644 index 0000000..3875fea --- /dev/null +++ b/src/Health/Checks/OpcacheHitRateCheck.php @@ -0,0 +1,81 @@ + + + Copyright © 2016-2026, Canyon GBS LLC. All rights reserved. + + Canyon GBS Common is licensed under the Elastic License 2.0. For more details, + see https://github.com/canyongbs/common/blob/main/LICENSE. + + Notice: + + - You may not provide the software to third parties as a hosted or managed + service, where the service provides users with access to any substantial set of + the features or functionality of the software. + - You may not move, change, disable, or circumvent the license key functionality + in the software, and you may not remove or obscure any functionality in the + software that is protected by the license key. + - You may not alter, remove, or obscure any licensing, copyright, or other notices + of the licensor in the software. Any use of the licensor’s trademarks is subject + to applicable law. + - Canyon GBS LLC respects the intellectual property rights of others and expects the + same in return. Canyon GBS™ and Canyon GBS Common are registered trademarks of + Canyon GBS LLC, and we are committed to enforcing and protecting our trademarks + vigorously. + - The software solution, including services, infrastructure, and code, is offered as a + Software as a Service (SaaS) by Canyon GBS LLC. + - Use of this software implies agreement to the license terms and conditions as stated + in the Elastic License 2.0. + + For more information or inquiries please visit our website at + https://www.canyongbs.com or contact us via email at legal@canyongbs.com. + + +*/ + +namespace CanyonGBS\Common\Health\Checks; + +use CanyonGBS\Common\Health\Services\OpcacheStatusService; +use Spatie\Health\Checks\Check; +use Spatie\Health\Checks\Result; + +class OpcacheHitRateCheck extends Check +{ + public function run(): Result + { + $status = app(OpcacheStatusService::class)->getStatus(); + + if ($status === false) { + return Result::make() + ->failed('OPcache is not available.'); + } + + $hits = $status['opcache_statistics']['hits'] ?? 0; + $misses = $status['opcache_statistics']['misses'] ?? 0; + $total = $hits + $misses; + + if ($total === 0) { + return Result::make() + ->shortSummary('No requests yet') + ->meta(['hits' => 0, 'misses' => 0, 'hit_rate' => null]) + ->ok(); + } + + $hitRate = round($hits / $total * 100, 2); + + $result = Result::make() + ->shortSummary("{$hitRate}%") + ->meta(['hits' => $hits, 'misses' => $misses, 'hit_rate' => $hitRate]); + + if ($hitRate <= 95) { + return $result->failed("OPcache hit rate is critically low ({$hitRate}%)"); + } + + if ($hitRate < 99) { + return $result->warning("OPcache hit rate is below optimal ({$hitRate}%)"); + } + + return $result->ok(); + } +} diff --git a/src/Health/Services/OpcacheStatusService.php b/src/Health/Services/OpcacheStatusService.php new file mode 100644 index 0000000..7680483 --- /dev/null +++ b/src/Health/Services/OpcacheStatusService.php @@ -0,0 +1,50 @@ + + + Copyright © 2016-2026, Canyon GBS LLC. All rights reserved. + + Canyon GBS Common is licensed under the Elastic License 2.0. For more details, + see https://github.com/canyongbs/common/blob/main/LICENSE. + + Notice: + + - You may not provide the software to third parties as a hosted or managed + service, where the service provides users with access to any substantial set of + the features or functionality of the software. + - You may not move, change, disable, or circumvent the license key functionality + in the software, and you may not remove or obscure any functionality in the + software that is protected by the license key. + - You may not alter, remove, or obscure any licensing, copyright, or other notices + of the licensor in the software. Any use of the licensor’s trademarks is subject + to applicable law. + - Canyon GBS LLC respects the intellectual property rights of others and expects the + same in return. Canyon GBS™ and Canyon GBS Common are registered trademarks of + Canyon GBS LLC, and we are committed to enforcing and protecting our trademarks + vigorously. + - The software solution, including services, infrastructure, and code, is offered as a + Software as a Service (SaaS) by Canyon GBS LLC. + - Use of this software implies agreement to the license terms and conditions as stated + in the Elastic License 2.0. + + For more information or inquiries please visit our website at + https://www.canyongbs.com or contact us via email at legal@canyongbs.com. + + +*/ + +namespace CanyonGBS\Common\Health\Services; + +class OpcacheStatusService +{ + /** @return array|false */ + public function getStatus(): array|false + { + if (! function_exists('opcache_get_status')) { + return false; + } + + return opcache_get_status(); + } +} diff --git a/tests/Health/OpcacheCachedFilesCheckTest.php b/tests/Health/OpcacheCachedFilesCheckTest.php new file mode 100644 index 0000000..2e3fdad --- /dev/null +++ b/tests/Health/OpcacheCachedFilesCheckTest.php @@ -0,0 +1,140 @@ + + + Copyright © 2016-2026, Canyon GBS LLC. All rights reserved. + + Canyon GBS Common is licensed under the Elastic License 2.0. For more details, + see https://github.com/canyongbs/common/blob/main/LICENSE. + + Notice: + + - You may not provide the software to third parties as a hosted or managed + service, where the service provides users with access to any substantial set of + the features or functionality of the software. + - You may not move, change, disable, or circumvent the license key functionality + in the software, and you may not remove or obscure any functionality in the + software that is protected by the license key. + - You may not alter, remove, or obscure any licensing, copyright, or other notices + of the licensor in the software. Any use of the licensor’s trademarks is subject + to applicable law. + - Canyon GBS LLC respects the intellectual property rights of others and expects the + same in return. Canyon GBS™ and Canyon GBS Common are registered trademarks of + Canyon GBS LLC, and we are committed to enforcing and protecting our trademarks + vigorously. + - The software solution, including services, infrastructure, and code, is offered as a + Software as a Service (SaaS) by Canyon GBS LLC. + - Use of this software implies agreement to the license terms and conditions as stated + in the Elastic License 2.0. + + For more information or inquiries please visit our website at + https://www.canyongbs.com or contact us via email at legal@canyongbs.com. + + +*/ + +use CanyonGBS\Common\Health\Checks\OpcacheCachedFilesCheck; +use CanyonGBS\Common\Health\Services\OpcacheStatusService; +use Mockery\MockInterface; +use Spatie\Health\Enums\Status; + +it('returns ok status when cached scripts are well below max', function () { + $this->mock(OpcacheStatusService::class, function (MockInterface $mock) { + $mock->shouldReceive('getStatus')->andReturn([ + 'opcache_statistics' => [ + 'num_cached_scripts' => 3000, + 'max_cached_keys' => 10000, + ], + ]); + }); + + $result = (new OpcacheCachedFilesCheck())->run(); + + expect($result->status)->toBe(Status::ok()); +}); + +it('returns warning status when cached scripts reach 90% of max', function () { + $this->mock(OpcacheStatusService::class, function (MockInterface $mock) { + $mock->shouldReceive('getStatus')->andReturn([ + 'opcache_statistics' => [ + 'num_cached_scripts' => 9000, + 'max_cached_keys' => 10000, + ], + ]); + }); + + $result = (new OpcacheCachedFilesCheck())->run(); + + expect($result->status)->toBe(Status::warning()); +}); + +it('returns warning status when cached scripts are between 90% and 100% of max', function () { + $this->mock(OpcacheStatusService::class, function (MockInterface $mock) { + $mock->shouldReceive('getStatus')->andReturn([ + 'opcache_statistics' => [ + 'num_cached_scripts' => 9500, + 'max_cached_keys' => 10000, + ], + ]); + }); + + $result = (new OpcacheCachedFilesCheck())->run(); + + expect($result->status)->toBe(Status::warning()); +}); + +it('returns failed status when cached scripts reach max', function () { + $this->mock(OpcacheStatusService::class, function (MockInterface $mock) { + $mock->shouldReceive('getStatus')->andReturn([ + 'opcache_statistics' => [ + 'num_cached_scripts' => 10000, + 'max_cached_keys' => 10000, + ], + ]); + }); + + $result = (new OpcacheCachedFilesCheck())->run(); + + expect($result->status)->toBe(Status::failed()); +}); + +it('returns failed status when cached scripts exceed max', function () { + $this->mock(OpcacheStatusService::class, function (MockInterface $mock) { + $mock->shouldReceive('getStatus')->andReturn([ + 'opcache_statistics' => [ + 'num_cached_scripts' => 10500, + 'max_cached_keys' => 10000, + ], + ]); + }); + + $result = (new OpcacheCachedFilesCheck())->run(); + + expect($result->status)->toBe(Status::failed()); +}); + +it('returns failed status when opcache is not available', function () { + $this->mock(OpcacheStatusService::class, function (MockInterface $mock) { + $mock->shouldReceive('getStatus')->andReturn(false); + }); + + $result = (new OpcacheCachedFilesCheck())->run(); + + expect($result->status)->toBe(Status::failed()); +}); + +it('returns failed status when max_cached_keys is zero', function () { + $this->mock(OpcacheStatusService::class, function (MockInterface $mock) { + $mock->shouldReceive('getStatus')->andReturn([ + 'opcache_statistics' => [ + 'num_cached_scripts' => 0, + 'max_cached_keys' => 0, + ], + ]); + }); + + $result = (new OpcacheCachedFilesCheck())->run(); + + expect($result->status)->toBe(Status::failed()); +}); diff --git a/tests/Health/OpcacheHitRateCheckTest.php b/tests/Health/OpcacheHitRateCheckTest.php new file mode 100644 index 0000000..58bc4b4 --- /dev/null +++ b/tests/Health/OpcacheHitRateCheckTest.php @@ -0,0 +1,125 @@ + + + Copyright © 2016-2026, Canyon GBS LLC. All rights reserved. + + Canyon GBS Common is licensed under the Elastic License 2.0. For more details, + see https://github.com/canyongbs/common/blob/main/LICENSE. + + Notice: + + - You may not provide the software to third parties as a hosted or managed + service, where the service provides users with access to any substantial set of + the features or functionality of the software. + - You may not move, change, disable, or circumvent the license key functionality + in the software, and you may not remove or obscure any functionality in the + software that is protected by the license key. + - You may not alter, remove, or obscure any licensing, copyright, or other notices + of the licensor in the software. Any use of the licensor’s trademarks is subject + to applicable law. + - Canyon GBS LLC respects the intellectual property rights of others and expects the + same in return. Canyon GBS™ and Canyon GBS Common are registered trademarks of + Canyon GBS LLC, and we are committed to enforcing and protecting our trademarks + vigorously. + - The software solution, including services, infrastructure, and code, is offered as a + Software as a Service (SaaS) by Canyon GBS LLC. + - Use of this software implies agreement to the license terms and conditions as stated + in the Elastic License 2.0. + + For more information or inquiries please visit our website at + https://www.canyongbs.com or contact us via email at legal@canyongbs.com. + + +*/ + +use CanyonGBS\Common\Health\Checks\OpcacheHitRateCheck; +use CanyonGBS\Common\Health\Services\OpcacheStatusService; +use Mockery\MockInterface; +use Spatie\Health\Enums\Status; + +it('returns ok status when hit rate is above 99%', function () { + $this->mock(OpcacheStatusService::class, function (MockInterface $mock) { + $mock->shouldReceive('getStatus')->andReturn([ + 'opcache_statistics' => [ + 'hits' => 9950, + 'misses' => 10, + ], + ]); + }); + + $result = (new OpcacheHitRateCheck())->run(); + + expect($result->status)->toBe(Status::ok()); +}); + +it('returns warning status when hit rate is below 99% but above 95%', function () { + $this->mock(OpcacheStatusService::class, function (MockInterface $mock) { + $mock->shouldReceive('getStatus')->andReturn([ + 'opcache_statistics' => [ + 'hits' => 970, + 'misses' => 30, + ], + ]); + }); + + $result = (new OpcacheHitRateCheck())->run(); + + expect($result->status)->toBe(Status::warning()); +}); + +it('returns failed status when hit rate is at or below 95%', function () { + $this->mock(OpcacheStatusService::class, function (MockInterface $mock) { + $mock->shouldReceive('getStatus')->andReturn([ + 'opcache_statistics' => [ + 'hits' => 950, + 'misses' => 50, + ], + ]); + }); + + $result = (new OpcacheHitRateCheck())->run(); + + expect($result->status)->toBe(Status::failed()); +}); + +it('returns failed status when hit rate is below 95%', function () { + $this->mock(OpcacheStatusService::class, function (MockInterface $mock) { + $mock->shouldReceive('getStatus')->andReturn([ + 'opcache_statistics' => [ + 'hits' => 800, + 'misses' => 200, + ], + ]); + }); + + $result = (new OpcacheHitRateCheck())->run(); + + expect($result->status)->toBe(Status::failed()); +}); + +it('returns ok status when there are no requests yet', function () { + $this->mock(OpcacheStatusService::class, function (MockInterface $mock) { + $mock->shouldReceive('getStatus')->andReturn([ + 'opcache_statistics' => [ + 'hits' => 0, + 'misses' => 0, + ], + ]); + }); + + $result = (new OpcacheHitRateCheck())->run(); + + expect($result->status)->toBe(Status::ok()); +}); + +it('returns failed status when opcache is not available', function () { + $this->mock(OpcacheStatusService::class, function (MockInterface $mock) { + $mock->shouldReceive('getStatus')->andReturn(false); + }); + + $result = (new OpcacheHitRateCheck())->run(); + + expect($result->status)->toBe(Status::failed()); +});