From 5636745b44ecaea4e1159188e54de59a6db038bd Mon Sep 17 00:00:00 2001 From: Grzegorz Drozd <1885137+GrzegorzDrozd@users.noreply.github.com> Date: Fri, 23 May 2025 18:47:49 +0200 Subject: [PATCH] Add utility classes and tests for Timer and Attribute tracking by id and object Introduced `TimerTrackerById`, `TimerTrackerByObject`, `AttributeTrackerById`, and `AttributeTrackerByObject` classes for managing timers and attributes based on id or object references. This will be used by auto instrumentation and metrics in contrib modules. --- src/SDK/Metrics/Util/TimerTrackerById.php | 24 ++++ src/SDK/Metrics/Util/TimerTrackerByObject.php | 29 +++++ src/SDK/Util/AttributeTrackerById.php | 55 ++++++++ src/SDK/Util/AttributeTrackerByObject.php | 61 +++++++++ .../SDK/Metrics/Util/TimerTrackerByIdTest.php | 71 ++++++++++ .../Metrics/Util/TimerTrackerByObjectTest.php | 99 ++++++++++++++ .../SDK/Util/AttributeTrackerByIdTest.php | 119 +++++++++++++++++ .../SDK/Util/AttributeTrackerByObjectTest.php | 122 ++++++++++++++++++ 8 files changed, 580 insertions(+) create mode 100644 src/SDK/Metrics/Util/TimerTrackerById.php create mode 100644 src/SDK/Metrics/Util/TimerTrackerByObject.php create mode 100644 src/SDK/Util/AttributeTrackerById.php create mode 100644 src/SDK/Util/AttributeTrackerByObject.php create mode 100644 tests/Unit/SDK/Metrics/Util/TimerTrackerByIdTest.php create mode 100644 tests/Unit/SDK/Metrics/Util/TimerTrackerByObjectTest.php create mode 100644 tests/Unit/SDK/Util/AttributeTrackerByIdTest.php create mode 100644 tests/Unit/SDK/Util/AttributeTrackerByObjectTest.php diff --git a/src/SDK/Metrics/Util/TimerTrackerById.php b/src/SDK/Metrics/Util/TimerTrackerById.php new file mode 100644 index 000000000..a842fdd11 --- /dev/null +++ b/src/SDK/Metrics/Util/TimerTrackerById.php @@ -0,0 +1,24 @@ +timers[$id] = microtime(true); + } + + public function durationMs(string|int $id): float + { + if (!isset($this->timers[$id])) { + return 0; + } + + return (microtime(true) - $this->timers[$id]) * 1000; + } +} diff --git a/src/SDK/Metrics/Util/TimerTrackerByObject.php b/src/SDK/Metrics/Util/TimerTrackerByObject.php new file mode 100644 index 000000000..a39305c09 --- /dev/null +++ b/src/SDK/Metrics/Util/TimerTrackerByObject.php @@ -0,0 +1,29 @@ +timers = new \WeakMap(); + } + + public function start(object $id): void + { + $this->timers[$id] = microtime(true); + } + + public function durationMs(object $id): float + { + if ($this->timers->offsetExists($id) === false) { + return 0; + } + + return (microtime(true) - $this->timers[$id]) * 1000; + } +} diff --git a/src/SDK/Util/AttributeTrackerById.php b/src/SDK/Util/AttributeTrackerById.php new file mode 100644 index 000000000..6b9d28812 --- /dev/null +++ b/src/SDK/Util/AttributeTrackerById.php @@ -0,0 +1,55 @@ +attributes[$id]); + } + + public function set(string|int $id, array $attributes): void + { + $this->attributes[$id] = $attributes; + } + + public function add(string|int $id, array $attributes): array + { + if (!isset($this->attributes[$id])) { + return $this->attributes[$id] = $attributes; + + } + + return $this->attributes[$id] = [...$this->attributes[$id], ...$attributes]; + } + + public function get(string|int $id): array + { + if (!isset($this->attributes[$id])) { + return []; + } + + return $this->attributes[$id]; + } + + public function append(string|int $id, string|int $key, mixed $value): void + { + $this->attributes[$id] ??= []; + $this->attributes[$id][$key] = $value; + } + + public function clear(string|int $id): void + { + unset($this->attributes[$id]); + } + + public function reset(): void + { + $this->attributes = []; + } +} diff --git a/src/SDK/Util/AttributeTrackerByObject.php b/src/SDK/Util/AttributeTrackerByObject.php new file mode 100644 index 000000000..281ea0cb1 --- /dev/null +++ b/src/SDK/Util/AttributeTrackerByObject.php @@ -0,0 +1,61 @@ +attributes = new \WeakMap(); + } + + public function has(object $id): bool + { + return $this->attributes->offsetExists($id); + } + + public function set(object $id, array $attributes): void + { + $this->attributes[$id] = $attributes; + } + + public function add(object $id, array $attributes): array + { + if ($this->attributes->offsetExists($id) === false) { + return $this->attributes[$id] = $attributes; + + } + + return $this->attributes[$id] = [...$this->attributes[$id], ...$attributes]; + } + + public function get(object $id): array + { + if ($this->attributes->offsetExists($id) === false) { + return []; + } + + return $this->attributes[$id]; + } + + public function append(object $id, string|int $key, mixed $value): void + { + $attributes = $this->attributes[$id] ?? []; + $attributes[$key] = $value; + $this->attributes[$id] = $attributes; + } + + public function clear(object $id): void + { + unset($this->attributes[$id]); + } + + public function reset(): void + { + $this->attributes = new \WeakMap(); + } +} diff --git a/tests/Unit/SDK/Metrics/Util/TimerTrackerByIdTest.php b/tests/Unit/SDK/Metrics/Util/TimerTrackerByIdTest.php new file mode 100644 index 000000000..41b2037a5 --- /dev/null +++ b/tests/Unit/SDK/Metrics/Util/TimerTrackerByIdTest.php @@ -0,0 +1,71 @@ +timerTracker = new TimerTrackerById(); + } + + public function test_start(): void + { + $id = 'test_id'; + $this->timerTracker->start($id); + + // Assert that the timer has started for the given ID. + // This might involve checking an internal state if available, + // or simply ensuring no error is thrown. + // For now, we'll assume a subsequent call to durationMs will indicate success. + $this->assertGreaterThan(0, $this->timerTracker->durationMs($id)); + } + + public function test_duration_ms(): void + { + $id = 'test_id_duration'; + $this->timerTracker->start($id); + + // Simulate some time passing + usleep(2000); // 2 millisecond + + $duration = $this->timerTracker->durationMs($id); + + // Assert that the duration is a positive number + $this->assertIsFloat($duration); + $this->assertGreaterThan(0, $duration); + } + + public function test_duration_ms_for_non_existent_id(): void + { + $id = 'non_existent_id'; + $duration = $this->timerTracker->durationMs($id); + + // Assert that duration for a non-existent ID is 0 or null, depending on implementation + $this->assertEquals(0, $duration); + } + + public function test_start_multiple_times_for_same_id(): void + { + $id = 'test_id_multiple'; + $this->timerTracker->start($id); + usleep(2500); + $firstDuration = $this->timerTracker->durationMs($id); + + $this->timerTracker->start($id); // Restart the timer + usleep(2000); + $secondDuration = $this->timerTracker->durationMs($id); + + // Assert that the second duration is significantly smaller than the first, + // indicating the timer was reset. + $this->assertGreaterThan(0, $secondDuration); + $this->assertLessThan($firstDuration, $secondDuration); // Second duration should be smaller + } +} diff --git a/tests/Unit/SDK/Metrics/Util/TimerTrackerByObjectTest.php b/tests/Unit/SDK/Metrics/Util/TimerTrackerByObjectTest.php new file mode 100644 index 000000000..f51faeb49 --- /dev/null +++ b/tests/Unit/SDK/Metrics/Util/TimerTrackerByObjectTest.php @@ -0,0 +1,99 @@ +timerTracker = new TimerTrackerByObject(); + } + + public function test_start(): void + { + $id = new stdClass(); + $this->timerTracker->start($id); + + // Assert that the timer has started for the given object. + // We can't directly inspect the WeakMap, but we can check if durationMs returns a non-zero value. + $this->assertGreaterThan(0, $this->timerTracker->durationMs($id)); + } + + public function test_duration_ms(): void + { + $id = new stdClass(); + $this->timerTracker->start($id); + + // Simulate some time passing + usleep(2000); // 2 millisecond + + $duration = $this->timerTracker->durationMs($id); + + // Assert that the duration is a positive number + $this->assertIsFloat($duration); + $this->assertGreaterThan(0, $duration); + } + + public function test_duration_ms_for_non_existent_object(): void + { + $id = new stdClass(); + $duration = $this->timerTracker->durationMs($id); + + // Assert that duration for a non-existent ID is 0 + $this->assertEquals(0, $duration); + } + + public function test_start_multiple_times_for_same_object(): void + { + $id = new stdClass(); + $this->timerTracker->start($id); + usleep(2500); + $firstDuration = $this->timerTracker->durationMs($id); + + $this->timerTracker->start($id); // Restart the timer + usleep(1000); + $secondDuration = $this->timerTracker->durationMs($id); + + // Assert that the second duration is significantly smaller than the first, + // indicating the timer was reset. + $this->assertGreaterThan(0, $secondDuration); + $this->assertLessThan($firstDuration, $secondDuration); // Second duration should be smaller + } + + public function test_timer_removes_entry_when_object_is_garbage_collected(): void + { + $id = new stdClass(); + $this->timerTracker->start($id); + + // Ensure the timer is active for the object + $this->assertGreaterThan(0, $this->timerTracker->durationMs($id)); + + // Unset the reference to the object, allowing it to be garbage collected + unset($id); + + // Although garbage collection is non-deterministic in PHP, + // for testing purposes, we can assume that if the WeakMap is working, + // eventually the entry will be removed. + // In a real-world scenario, we might need to force GC or use a more + // elaborate testing mechanism if this assertion fails intermittently. + // For now, we'll just check immediately. + $dummyObject = new stdClass(); + $this->assertEquals(0, $this->timerTracker->durationMs($dummyObject)); + // Note: It's hard to directly test if the original object's entry is gone + // without keeping a reference to it. The key here is that if a timer was + // started for an object that is no longer referenced, subsequent calls + // with *that specific object* (if it were somehow recreated with the same + // internal ID, which is unlikely) or any other object will correctly + // return 0 if that object was not started. + // A more direct test would involve an internal WeakMap check if possible, + // but that breaks encapsulation. + } +} diff --git a/tests/Unit/SDK/Util/AttributeTrackerByIdTest.php b/tests/Unit/SDK/Util/AttributeTrackerByIdTest.php new file mode 100644 index 000000000..b8a512386 --- /dev/null +++ b/tests/Unit/SDK/Util/AttributeTrackerByIdTest.php @@ -0,0 +1,119 @@ +tracker = new AttributeTrackerById(); + } + + public function test_set(): void + { + $id = 'test_id_1'; + $attributes = ['key1' => 'value1', 'key2' => 123]; + $this->tracker->set($id, $attributes); + $this->assertTrue($this->tracker->has($id)); + $this->assertEquals($attributes, $this->tracker->get($id)); + + $newAttributes = ['key3' => true]; + $this->tracker->set($id, $newAttributes); // Overwrite + $this->assertEquals($newAttributes, $this->tracker->get($id)); + } + + public function test_get(): void + { + $id = 'test_id_2'; + $this->assertEmpty($this->tracker->get($id)); // Should return empty array if not set + + $attributes = ['name' => 'John', 'age' => 30]; + $this->tracker->set($id, $attributes); + $this->assertEquals($attributes, $this->tracker->get($id)); + } + + public function test_add(): void + { + $id = 'test_id_3'; + $initialAttributes = ['color' => 'red']; + $this->tracker->add($id, $initialAttributes); + $this->assertEquals($initialAttributes, $this->tracker->get($id)); + + $additionalAttributes = ['size' => 'M', 'color' => 'blue']; // 'color' should be overwritten + $result = $this->tracker->add($id, $additionalAttributes); + $expected = ['color' => 'blue', 'size' => 'M']; // Note: order might vary but keys/values should match + $this->assertEquals($expected, $this->tracker->get($id)); + $this->assertEquals($expected, $result); // 'add' method returns the merged array + + $id2 = 'test_id_4'; + $result2 = $this->tracker->add($id2, ['item' => 'book']); + $this->assertEquals(['item' => 'book'], $this->tracker->get($id2)); + $this->assertEquals(['item' => 'book'], $result2); + } + + public function test_reset(): void + { + $this->tracker->set('id1', ['a' => 1]); + $this->tracker->set('id2', ['b' => 2]); + $this->assertTrue($this->tracker->has('id1')); + $this->assertTrue($this->tracker->has('id2')); + + $this->tracker->reset(); + $this->assertFalse($this->tracker->has('id1')); + $this->assertFalse($this->tracker->has('id2')); + $this->assertEmpty($this->tracker->get('id1')); + } + + public function test_has(): void + { + $id = 'test_id_5'; + $this->assertFalse($this->tracker->has($id)); + + $this->tracker->set($id, ['data' => 'some_data']); + $this->assertTrue($this->tracker->has($id)); + + $this->tracker->clear($id); + $this->assertFalse($this->tracker->has($id)); + } + + public function test_append(): void + { + $id = 'test_id_6'; + $this->tracker->append($id, 'first_key', 'first_value'); + $this->assertEquals(['first_key' => 'first_value'], $this->tracker->get($id)); + + $this->tracker->append($id, 'second_key', 456); + $this->assertEquals(['first_key' => 'first_value', 'second_key' => 456], $this->tracker->get($id)); + + $this->tracker->append($id, 'first_key', 'new_value'); // Overwrite + $this->assertEquals(['first_key' => 'new_value', 'second_key' => 456], $this->tracker->get($id)); + + // Test with a numeric key + $this->tracker->append($id, 0, 'numeric_value'); + $this->assertEquals(['first_key' => 'new_value', 'second_key' => 456, 0 => 'numeric_value'], $this->tracker->get($id)); + } + + public function test_clear(): void + { + $id = 'test_id_7'; + $this->tracker->set($id, ['foo' => 'bar']); + $this->assertTrue($this->tracker->has($id)); + $this->assertEquals(['foo' => 'bar'], $this->tracker->get($id)); + + $this->tracker->clear($id); + $this->assertFalse($this->tracker->has($id)); + $this->assertEmpty($this->tracker->get($id)); + + // Clearing a non-existent ID should not cause an error + $this->tracker->clear('non_existent_id'); + $this->assertFalse($this->tracker->has('non_existent_id')); + } +} diff --git a/tests/Unit/SDK/Util/AttributeTrackerByObjectTest.php b/tests/Unit/SDK/Util/AttributeTrackerByObjectTest.php new file mode 100644 index 000000000..05dd21795 --- /dev/null +++ b/tests/Unit/SDK/Util/AttributeTrackerByObjectTest.php @@ -0,0 +1,122 @@ +tracker = new AttributeTrackerByObject(); + } + + public function test_set(): void + { + $id = new \stdClass(); + $attributes = ['key1' => 'value1', 'key2' => 123]; + $this->tracker->set($id, $attributes); + $this->assertTrue($this->tracker->has($id)); + $this->assertEquals($attributes, $this->tracker->get($id)); + + $newAttributes = ['key3' => true]; + $this->tracker->set($id, $newAttributes); // Overwrite + $this->assertEquals($newAttributes, $this->tracker->get($id)); + } + + public function test_get(): void + { + $id = new \stdClass(); + $this->assertEmpty($this->tracker->get($id)); // Should return empty array if not set + + $attributes = ['name' => 'Jane', 'age' => 25]; + $this->tracker->set($id, $attributes); + $this->assertEquals($attributes, $this->tracker->get($id)); + } + + public function test_add(): void + { + $id = new \stdClass(); + $initialAttributes = ['color' => 'green']; + $this->tracker->add($id, $initialAttributes); + $this->assertEquals($initialAttributes, $this->tracker->get($id)); + + $additionalAttributes = ['size' => 'L', 'color' => 'yellow']; // 'color' should be overwritten + $result = $this->tracker->add($id, $additionalAttributes); + $expected = ['color' => 'yellow', 'size' => 'L']; + $this->assertEquals($expected, $this->tracker->get($id)); + $this->assertEquals($expected, $result); // 'add' method returns the merged array + + $id2 = new \stdClass(); + $result2 = $this->tracker->add($id2, ['item' => 'pen']); + $this->assertEquals(['item' => 'pen'], $this->tracker->get($id2)); + $this->assertEquals(['item' => 'pen'], $result2); + } + + public function test_reset(): void + { + $id1 = new \stdClass(); + $id2 = new \stdClass(); + $this->tracker->set($id1, ['a' => 1]); + $this->tracker->set($id2, ['b' => 2]); + $this->assertTrue($this->tracker->has($id1)); + $this->assertTrue($this->tracker->has($id2)); + + $this->tracker->reset(); + $this->assertFalse($this->tracker->has($id1)); // WeakMap might still hold references for a short while + $this->assertFalse($this->tracker->has($id2)); // but functionally, it's reset + $this->assertEmpty($this->tracker->get($id1)); + } + + public function test_has(): void + { + $id = new \stdClass(); + $this->assertFalse($this->tracker->has($id)); + + $this->tracker->set($id, ['data' => 'some_object_data']); + $this->assertTrue($this->tracker->has($id)); + + $this->tracker->clear($id); + $this->assertFalse($this->tracker->has($id)); + } + + public function test_append(): void + { + $id = new \stdClass(); + $this->tracker->append($id, 'first_key', 'first_value'); + $this->assertEquals(['first_key' => 'first_value'], $this->tracker->get($id)); + + $this->tracker->append($id, 'second_key', 789); + $this->assertEquals(['first_key' => 'first_value', 'second_key' => 789], $this->tracker->get($id)); + + $this->tracker->append($id, 'first_key', 'another_value'); // Overwrite + $this->assertEquals(['first_key' => 'another_value', 'second_key' => 789], $this->tracker->get($id)); + + // Test with a numeric key + $this->tracker->append($id, 0, 'numeric_object_value'); + $this->assertEquals(['first_key' => 'another_value', 'second_key' => 789, 0 => 'numeric_object_value'], $this->tracker->get($id)); + } + + public function test_clear(): void + { + $id = new \stdClass(); + $this->tracker->set($id, ['bar' => 'baz']); + $this->assertTrue($this->tracker->has($id)); + $this->assertEquals(['bar' => 'baz'], $this->tracker->get($id)); + + $this->tracker->clear($id); + $this->assertFalse($this->tracker->has($id)); + $this->assertEmpty($this->tracker->get($id)); + + // Clearing a non-existent ID should not cause an error + $nonExistentId = new \stdClass(); + $this->tracker->clear($nonExistentId); + $this->assertFalse($this->tracker->has($nonExistentId)); + } +}