From 27161ccf27f81681bce11720fc189a6616b3fcd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 12 May 2026 23:21:05 +0300 Subject: [PATCH 1/5] Test Octane request context provider - Cover captured request and env snapshot behavior. - Lock active request replacement and missing-state failure. --- .../OctaneRequestContextProviderTest.php | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/Octane/OctaneRequestContextProviderTest.php diff --git a/tests/Octane/OctaneRequestContextProviderTest.php b/tests/Octane/OctaneRequestContextProviderTest.php new file mode 100644 index 0000000..4eeeb15 --- /dev/null +++ b/tests/Octane/OctaneRequestContextProviderTest.php @@ -0,0 +1,70 @@ + '2'], [], [], [ + 'REQUEST_URI' => '/profiles?page=2', + 'REQUEST_METHOD' => 'GET', + 'REQUEST_TIME' => 1234, + 'REQUEST_TIME_FLOAT' => 1234.5, + ]); + + $state = new OctaneRequestContextState; + $state->activate($request, ['APP_ENV' => 'testing']); + + $provider = new OctaneRequestContextProvider($state); + $context = $provider->capture(); + + $this->assertInstanceOf(RequestContextInterface::class, $context); + $this->assertSame('/profiles?page=2', $context->getUrl()); + $this->assertSame(['page' => '2'], $context->getQuery()); + $this->assertSame(['APP_ENV' => 'testing'], $context->getEnv()); + $this->assertSame(1234.5, $context->getServer()['REQUEST_TIME_FLOAT']); + $this->assertSame(1234, $context->getServer()['REQUEST_TIME']); + } + + public function test_capture_uses_the_current_active_request_snapshot(): void + { + $state = new OctaneRequestContextState; + $provider = new OctaneRequestContextProvider($state); + + $state->activate(Request::create('/first', 'GET', ['page' => '1'], [], [], [ + 'REQUEST_TIME' => 100, + 'REQUEST_TIME_FLOAT' => 100.0, + ]), ['APP_ENV' => 'testing']); + $firstContext = $provider->capture(); + + $state->clear(); + $state->activate(Request::create('/second', 'GET', ['page' => '2'], [], [], [ + 'REQUEST_TIME' => 200, + 'REQUEST_TIME_FLOAT' => 200.0, + ]), ['APP_ENV' => 'testing']); + $secondContext = $provider->capture(); + + $this->assertSame('/first?page=1', $firstContext->getUrl()); + $this->assertSame('/second?page=2', $secondContext->getUrl()); + $this->assertSame(['page' => '2'], $secondContext->getQuery()); + $this->assertSame(200.0, $secondContext->getServer()['REQUEST_TIME_FLOAT']); + } + + public function test_capture_fails_without_an_active_request_snapshot(): void + { + $provider = new OctaneRequestContextProvider(new OctaneRequestContextState); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Octane request context is not active.'); + + $provider->capture(); + } +} From d08ceb96e39874ff36312c67904ed6bf35d7db25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 12 May 2026 23:21:05 +0300 Subject: [PATCH 2/5] Test Octane profiler manager lifecycle - Cover profiler reuse and stale worker recovery. - Verify cleanup and request-context handling on failures. --- .../OctaneWorkerProfilerManagerTest.php | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 tests/Octane/OctaneWorkerProfilerManagerTest.php diff --git a/tests/Octane/OctaneWorkerProfilerManagerTest.php b/tests/Octane/OctaneWorkerProfilerManagerTest.php new file mode 100644 index 0000000..533dd07 --- /dev/null +++ b/tests/Octane/OctaneWorkerProfilerManagerTest.php @@ -0,0 +1,150 @@ +newManager(); + + $profilerA = $manager->startRequest($this->enabledConfig(), Request::create('/profiles', 'GET', ['page' => '2'])); + $manager->stopRequest(); + + $profilerB = $manager->startRequest($this->enabledConfig(), Request::create('/health', 'GET', ['ping' => '1'])); + $manager->stopRequest(); + + $this->assertSame($profilerA, $profilerB); + } + + public function test_stale_running_profiler_is_stopped_and_rebuilt_before_the_next_request(): void + { + $logger = new class + { + public array $warnings = []; + + public function warning(string $message): void + { + $this->warnings[] = $message; + } + }; + + Log::swap($logger); + + $firstProfilerState = $this->newProfilerState(); + $manager = $this->newManager([$firstProfilerState, $this->newProfilerState()]); + + $profilerA = $manager->startRequest($this->enabledConfig(), Request::create('/first')); + $profilerB = $manager->startRequest($this->enabledConfig(), Request::create('/second')); + + $this->assertNotSame($profilerA, $profilerB); + $this->assertTrue($firstProfilerState->stopped); + $this->assertSame( + ['Xhgui Octane profiler was still running at the start of a new request; rebuilding worker profiler state.'], + $logger->warnings, + ); + } + + public function test_enable_failure_clears_the_active_request_context(): void + { + $profilerState = $this->newProfilerState([ + 'enableException' => new \RuntimeException('boom'), + ]); + $state = new OctaneRequestContextState; + $provider = new OctaneRequestContextProvider($state); + $manager = $this->newManager([$profilerState], $state, $provider); + + try { + $manager->startRequest($this->enabledConfig(), Request::create('/broken')); + $this->fail('Expected the profiler enable failure to be rethrown.'); + } catch (\RuntimeException $exception) { + $this->assertSame('boom', $exception->getMessage()); + } + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Octane request context is not active.'); + + $provider->capture(); + } + + public function test_stale_cleanup_failures_are_logged_before_rebuild(): void + { + $logger = new class + { + public array $warnings = []; + + public function warning(string $message): void + { + $this->warnings[] = $message; + } + }; + + Log::swap($logger); + + $firstProfilerState = $this->newProfilerState([ + 'disableException' => new \RuntimeException('cleanup failed'), + ]); + $manager = $this->newManager([$firstProfilerState, $this->newProfilerState()]); + + $profilerA = $manager->startRequest($this->enabledConfig(), Request::create('/first')); + $profilerB = $manager->startRequest($this->enabledConfig(), Request::create('/second')); + + $this->assertNotSame($profilerA, $profilerB); + $this->assertSame([ + 'Xhgui Octane profiler was still running at the start of a new request; rebuilding worker profiler state.', + 'Xhgui Octane stale profiler cleanup failed: cleanup failed', + ], $logger->warnings); + } + + private function newManager( + array $profilerStates = [], + ?OctaneRequestContextState $state = null, + ?OctaneRequestContextProvider $provider = null, + ): OctaneWorkerProfilerManager { + $state ??= new OctaneRequestContextState; + $provider ??= new OctaneRequestContextProvider($state); + + return new class($state, $provider, $profilerStates, $this) extends OctaneWorkerProfilerManager + { + public function __construct( + OctaneRequestContextState $state, + private OctaneRequestContextProvider $provider, + private array $profilerStates, + private OctaneWorkerProfilerManagerTest $test, + ) { + parent::__construct($state, $provider); + } + + protected function createProfiler(array $config): Profiler + { + $profilerState = array_shift($this->profilerStates); + + return $this->test->makeManagedProfiler($this->provider, $profilerState); + } + }; + } + + public function makeManagedProfiler(OctaneRequestContextProvider $provider, ?object $state = null): Profiler + { + return $this->newManagedProfiler($provider, $state); + } + + private function enabledConfig(array $overrides = []): array + { + return array_replace_recursive([ + 'enabled' => true, + 'save.handler' => Profiler::SAVER_FILE, + 'save.handler.file' => [ + 'filename' => sys_get_temp_dir().'/xhgui-manager.jsonl', + ], + ], $overrides); + } +} From 3b593ea06c885e1b4041aa7f938c25cb22097edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Mon, 11 May 2026 23:47:58 +0300 Subject: [PATCH 3/5] Test: Cover manager-driven Octane provider flow - Assert provider binding and sandbox profiler handoff through the manager - Lock disabled-request recovery and safe stop failure logging behavior --- .../XhguiProfilerOctaneLifecycleTest.php | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 tests/Octane/XhguiProfilerOctaneLifecycleTest.php diff --git a/tests/Octane/XhguiProfilerOctaneLifecycleTest.php b/tests/Octane/XhguiProfilerOctaneLifecycleTest.php new file mode 100644 index 0000000..24bf18e --- /dev/null +++ b/tests/Octane/XhguiProfilerOctaneLifecycleTest.php @@ -0,0 +1,189 @@ +app); + + $provider->register(); + + $this->assertTrue($this->app->bound(OctaneWorkerProfilerManager::class)); + $this->assertFalse($this->app->resolved(OctaneWorkerProfilerManager::class)); + $this->assertSame( + $this->app->make(OctaneWorkerProfilerManager::class), + $this->app->make(OctaneWorkerProfilerManager::class), + ); + } + + public function test_request_start_returns_early_when_disabled(): void + { + $sandbox = $this->newSandbox(['enabled' => false]); + + $this->bootProvider(); + $this->app['events']->dispatch($this->requestReceived($sandbox)); + + $this->assertFalse($this->app->resolved(OctaneWorkerProfilerManager::class)); + $this->assertFalse($sandbox->bound(Profiler::class)); + } + + public function test_disabled_requests_still_recover_stale_worker_profilers(): void + { + $manager = $this->createMock(OctaneWorkerProfilerManager::class); + $manager->expects($this->once()) + ->method('recoverStaleStateIfRunning'); + + $enabledSandbox = $this->newSandbox(['enabled' => true]); + $disabledSandbox = $this->newSandbox(['enabled' => false]); + + $this->bootProvider($manager); + $this->app['events']->dispatch($this->requestReceived($enabledSandbox)); + $this->app['events']->dispatch($this->requestReceived($disabledSandbox)); + + $this->assertFalse($disabledSandbox->bound(Profiler::class)); + } + + public function test_request_start_binds_the_manager_profiler_on_the_sandbox(): void + { + $request = Request::create('/hello?foo=bar', 'GET'); + $profiler = new Profiler([]); + $manager = $this->createMock(OctaneWorkerProfilerManager::class); + $manager->expects($this->once()) + ->method('startRequest') + ->with($this->callback(fn (array $config): bool => ($config['enabled'] ?? null) === true), $request) + ->willReturn($profiler); + + $sandbox = $this->newSandbox(['enabled' => true]); + + $this->bootProvider($manager); + $this->app['events']->dispatch($this->requestReceived($sandbox, $request)); + + $this->assertSame($profiler, $sandbox->make(Profiler::class)); + $this->assertFalse($this->app->bound(Profiler::class)); + } + + public function test_request_start_logs_failures_without_breaking_the_request(): void + { + Log::shouldReceive('error') + ->once() + ->with('Xhgui profiler initialization failed', $this->callback(function (array $context): bool { + return ($context['exception'] ?? null) instanceof RuntimeException + && $context['exception']->getMessage() === 'boom'; + })); + + $manager = $this->createMock(OctaneWorkerProfilerManager::class); + $manager->expects($this->once()) + ->method('startRequest') + ->willThrowException(new RuntimeException('boom')); + + $sandbox = $this->newSandbox(['enabled' => true]); + + $this->bootProvider($manager); + $this->app['events']->dispatch($this->requestReceived($sandbox)); + + $this->assertFalse($sandbox->bound(Profiler::class)); + } + + public function test_request_stop_calls_stop_on_the_manager_and_clears_the_sandbox_binding(): void + { + $profiler = new Profiler([]); + $manager = $this->createMock(OctaneWorkerProfilerManager::class); + $manager->expects($this->once()) + ->method('startRequest') + ->willReturn($profiler); + $manager->expects($this->once()) + ->method('stopRequest'); + + $sandbox = $this->newSandbox(['enabled' => true]); + + $this->bootProvider($manager); + $this->app['events']->dispatch($this->requestReceived($sandbox)); + $this->app['events']->dispatch($this->requestTerminated($sandbox)); + + $this->assertFalse($sandbox->bound(Profiler::class)); + } + + public function test_request_stop_logs_failures_and_clears_the_sandbox_binding(): void + { + Log::shouldReceive('error') + ->once() + ->with('Xhgui profiler stop failed', $this->callback(function (array $context): bool { + return ($context['exception'] ?? null) instanceof RuntimeException + && $context['exception']->getMessage() === 'stop failed'; + })); + + $profiler = new Profiler([]); + $manager = $this->createMock(OctaneWorkerProfilerManager::class); + $manager->expects($this->once()) + ->method('startRequest') + ->willReturn($profiler); + $manager->expects($this->once()) + ->method('stopRequest') + ->willThrowException(new RuntimeException('stop failed')); + + $sandbox = $this->newSandbox(['enabled' => true]); + + $this->bootProvider($manager); + $this->app['events']->dispatch($this->requestReceived($sandbox)); + $this->app['events']->dispatch($this->requestTerminated($sandbox)); + + $this->assertFalse($sandbox->bound(Profiler::class)); + } + + private function bootProvider(?OctaneWorkerProfilerManager $manager = null): XhguiProfilerServiceProvider + { + $provider = new XhguiProfilerServiceProvider($this->app); + $provider->register(); + + if ($manager !== null) { + $this->app->instance(OctaneWorkerProfilerManager::class, $manager); + } + + $provider->boot($this->app['events']); + + return $provider; + } + + /** + * @param array $xhguiConfig + */ + private function newSandbox(array $xhguiConfig): LaravelApplication + { + $sandbox = new LaravelApplication($this->app->basePath()); + $sandbox->instance('config', new Repository([ + 'xhgui' => $xhguiConfig, + ])); + + return $sandbox; + } + + private function requestReceived(LaravelApplication $sandbox, ?Request $request = null): RequestReceived + { + return new RequestReceived($this->app, $sandbox, $request ?? Request::create('/')); + } + + private function requestTerminated(LaravelApplication $sandbox): RequestTerminated + { + return new RequestTerminated( + $this->app, + $sandbox, + Request::create('/'), + new Response, + ); + } +} From 22f87e48e3429e32112b6052066ab15866d69433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Wed, 20 May 2026 00:39:04 +0300 Subject: [PATCH 4/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/Octane/OctaneWorkerProfilerManagerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Octane/OctaneWorkerProfilerManagerTest.php b/tests/Octane/OctaneWorkerProfilerManagerTest.php index 533dd07..78050ef 100644 --- a/tests/Octane/OctaneWorkerProfilerManagerTest.php +++ b/tests/Octane/OctaneWorkerProfilerManagerTest.php @@ -69,7 +69,7 @@ public function test_enable_failure_clears_the_active_request_context(): void $this->assertSame('boom', $exception->getMessage()); } - $this->expectException(\RuntimeException::class); + $this->expectException(\Xhgui\Profiler\Laravel\LaravelProfilerException::class); $this->expectExceptionMessage('Octane request context is not active.'); $provider->capture(); From 57a23c0b8e6e7d63ce44c85dd5ff4ef8b502dedb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 21:46:12 +0000 Subject: [PATCH 5/5] test: assert LaravelProfilerException in Octane context tests Agent-Logs-Url: https://github.com/perftools/php-profiler-laravel/sessions/e93f4de5-71ef-4fe6-b263-91ee5b033611 Co-authored-by: glensc <199095+glensc@users.noreply.github.com> --- tests/Octane/OctaneRequestContextProviderTest.php | 4 ++-- tests/Octane/OctaneWorkerProfilerManagerTest.php | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Octane/OctaneRequestContextProviderTest.php b/tests/Octane/OctaneRequestContextProviderTest.php index 4eeeb15..c10a001 100644 --- a/tests/Octane/OctaneRequestContextProviderTest.php +++ b/tests/Octane/OctaneRequestContextProviderTest.php @@ -3,7 +3,7 @@ namespace Xhgui\Profiler\Laravel\Tests\Octane; use Illuminate\Http\Request; -use RuntimeException; +use Xhgui\Profiler\Laravel\Exception\LaravelProfilerException; use Xhgui\Profiler\Laravel\Octane\OctaneRequestContextProvider; use Xhgui\Profiler\Laravel\Octane\OctaneRequestContextState; use Xhgui\Profiler\Laravel\Tests\TestCase; @@ -62,7 +62,7 @@ public function test_capture_fails_without_an_active_request_snapshot(): void { $provider = new OctaneRequestContextProvider(new OctaneRequestContextState); - $this->expectException(RuntimeException::class); + $this->expectException(LaravelProfilerException::class); $this->expectExceptionMessage('Octane request context is not active.'); $provider->capture(); diff --git a/tests/Octane/OctaneWorkerProfilerManagerTest.php b/tests/Octane/OctaneWorkerProfilerManagerTest.php index 78050ef..9ee7bbf 100644 --- a/tests/Octane/OctaneWorkerProfilerManagerTest.php +++ b/tests/Octane/OctaneWorkerProfilerManagerTest.php @@ -4,6 +4,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; +use Xhgui\Profiler\Laravel\Exception\LaravelProfilerException; use Xhgui\Profiler\Laravel\Octane\OctaneRequestContextProvider; use Xhgui\Profiler\Laravel\Octane\OctaneRequestContextState; use Xhgui\Profiler\Laravel\Octane\OctaneWorkerProfilerManager; @@ -69,7 +70,7 @@ public function test_enable_failure_clears_the_active_request_context(): void $this->assertSame('boom', $exception->getMessage()); } - $this->expectException(\Xhgui\Profiler\Laravel\LaravelProfilerException::class); + $this->expectException(LaravelProfilerException::class); $this->expectExceptionMessage('Octane request context is not active.'); $provider->capture();