originalServer = $_SERVER; $this->collector = $this->createInstance(); } protected function tearDown(): void { $_SERVER = $this->originalServer; $this->resetSingleton(); parent::tearDown(); } // ── Singleton ──────────────────────────────────────────────────── #[Test] public function get_instance_returns_singleton(): void { $instance1 = RuntimeCollector::get_instance(); $instance2 = RuntimeCollector::get_instance(); $this->assertSame($instance1, $instance2); } #[Test] public function get_instance_returns_runtime_collector(): void { $instance = RuntimeCollector::get_instance(); $this->assertInstanceOf(RuntimeCollector::class, $instance); } // ── get_stored_metrics() ───────────────────────────────────────── #[Test] public function get_stored_metrics_returns_default_when_empty(): void { $metrics = $this->collector->get_stored_metrics(); $this->assertSame([], $metrics); } #[Test] public function get_stored_metrics_returns_stored_data(): void { $stored = [ 'counters' => ['key' => ['name' => 'test', 'value' => 5]], 'histograms' => [], 'last_reset' => 1000000, ]; GlobalFunctionState::$options['wp_prometheus_runtime_metrics'] = $stored; $metrics = $this->collector->get_stored_metrics(); $this->assertSame($stored, $metrics); } #[Test] public function get_stored_metrics_returns_default_for_non_array(): void { GlobalFunctionState::$options['wp_prometheus_runtime_metrics'] = 'invalid'; $metrics = $this->collector->get_stored_metrics(); $this->assertArrayHasKey('counters', $metrics); $this->assertArrayHasKey('histograms', $metrics); $this->assertArrayHasKey('last_reset', $metrics); } // ── reset_metrics() ────────────────────────────────────────────── #[Test] public function reset_metrics_deletes_option(): void { GlobalFunctionState::$options['wp_prometheus_runtime_metrics'] = ['data']; RuntimeCollector::reset_metrics(); $this->assertSame(1, GlobalFunctionState::getCallCount('delete_option')); $this->assertArrayNotHasKey( 'wp_prometheus_runtime_metrics', GlobalFunctionState::$options ); } // ── get_duration_buckets() ─────────────────────────────────────── #[Test] public function get_duration_buckets_returns_expected_values(): void { $buckets = RuntimeCollector::get_duration_buckets(); $this->assertIsArray($buckets); $this->assertCount(11, $buckets); $this->assertSame(0.005, $buckets[0]); $this->assertEquals(10, end($buckets)); } #[Test] public function duration_buckets_are_in_ascending_order(): void { $buckets = RuntimeCollector::get_duration_buckets(); $sorted = $buckets; sort($sorted); $this->assertSame($sorted, $buckets); } // ── get_normalized_endpoint() (private, via reflection) ────────── #[Test] public function normalized_endpoint_returns_admin_when_is_admin(): void { $_SERVER['REQUEST_URI'] = '/wp-admin/options-general.php'; GlobalFunctionState::$options['__is_admin'] = true; $result = $this->callPrivateMethod('get_normalized_endpoint'); $this->assertSame('admin', $result); } #[Test] public function normalized_endpoint_returns_ajax_when_doing_ajax(): void { $_SERVER['REQUEST_URI'] = '/wp-admin/admin-ajax.php'; GlobalFunctionState::$options['__wp_doing_ajax'] = true; $result = $this->callPrivateMethod('get_normalized_endpoint'); $this->assertSame('ajax', $result); } #[Test] public function normalized_endpoint_returns_cron_when_doing_cron(): void { // Use a generic URI (not /wp-cron.php) to ensure function is checked, not URL pattern. $_SERVER['REQUEST_URI'] = '/some-page'; GlobalFunctionState::$options['__wp_doing_cron'] = true; $result = $this->callPrivateMethod('get_normalized_endpoint'); $this->assertSame('cron', $result); } #[Test] #[DataProvider('urlEndpointProvider')] public function normalized_endpoint_from_url_pattern(string $uri, string $expected): void { $_SERVER['REQUEST_URI'] = $uri; $result = $this->callPrivateMethod('get_normalized_endpoint'); $this->assertSame($expected, $result); } public static function urlEndpointProvider(): array { return [ 'rest api' => ['/wp-json/wp/v2/posts', 'rest-api'], 'login page' => ['/wp-login.php', 'login'], 'login with query' => ['/wp-login.php?action=login', 'login'], 'wp-cron' => ['/wp-cron.php', 'cron'], 'feed root' => ['/feed/', 'feed'], 'feed trailing' => ['/category/news/feed', 'feed'], 'feed with slash' => ['/feed', 'feed'], 'homepage' => ['/', 'frontend'], 'page' => ['/about-us', 'frontend'], 'post' => ['/2024/01/hello-world', 'frontend'], ]; } // ── is_metrics_request() (private, via reflection) ─────────────── #[Test] public function is_metrics_request_true_for_metrics_uri(): void { $_SERVER['REQUEST_URI'] = '/metrics'; $this->assertTrue($this->callPrivateMethod('is_metrics_request')); } #[Test] public function is_metrics_request_true_with_trailing_slash(): void { $_SERVER['REQUEST_URI'] = '/metrics/'; $this->assertTrue($this->callPrivateMethod('is_metrics_request')); } #[Test] public function is_metrics_request_false_for_other_uri(): void { $_SERVER['REQUEST_URI'] = '/some-page'; $this->assertFalse($this->callPrivateMethod('is_metrics_request')); } #[Test] public function is_metrics_request_false_when_no_uri(): void { unset($_SERVER['REQUEST_URI']); $this->assertFalse($this->callPrivateMethod('is_metrics_request')); } // ── Helpers ────────────────────────────────────────────────────── private function createInstance(): RuntimeCollector { $reflection = new \ReflectionClass(RuntimeCollector::class); return $reflection->newInstanceWithoutConstructor(); } private function resetSingleton(): void { $reflection = new \ReflectionClass(RuntimeCollector::class); $property = $reflection->getProperty('instance'); $property->setValue(null, null); } private function callPrivateMethod(string $method, array $args = []): mixed { $reflection = new \ReflectionMethod($this->collector, $method); return $reflection->invoke($this->collector, ...$args); } }