From e49cff7f85f1bf7a71e4615237ea625b6099ea04 Mon Sep 17 00:00:00 2001 From: lucilastancato Date: Fri, 24 Oct 2025 13:12:09 -0300 Subject: [PATCH 01/28] update CappedPostRanker constructor args to pass optional params last --- .../StreamBuilder/StreamRankers/CappedPostRanker.php | 8 ++++---- .../StreamBuilder/StreamRankers/CappedPostRankerTest.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/Tumblr/StreamBuilder/StreamRankers/CappedPostRanker.php b/lib/Tumblr/StreamBuilder/StreamRankers/CappedPostRanker.php index c62407b..f0fb0e3 100644 --- a/lib/Tumblr/StreamBuilder/StreamRankers/CappedPostRanker.php +++ b/lib/Tumblr/StreamBuilder/StreamRankers/CappedPostRanker.php @@ -61,20 +61,20 @@ class CappedPostRanker extends StreamRanker * CappedPostRanker constructor. * @param User $user User * @param string $identity The identity of this ranker - * @param bool $debug Need extra debug infos or not. e.g. Ranking score * @param bool $cap_desc Whether we are looking for the first violated blog post in descending order (top-down) * @param int $cap The cap applied (higher values are making the reranking less strict) * @param string $ranking_context The context that ranking is being applied to e.g. dashboard * @param bool $panel_allow_ranking Whether the meta key on panel regarding ranking is enabled or not + * @param bool $debug Need extra debug infos or not. e.g. Ranking score */ public function __construct( User $user, string $identity, - ?bool $debug = null, bool $cap_desc, int $cap, string $ranking_context, - bool $panel_allow_ranking + bool $panel_allow_ranking, + ?bool $debug = null ) { parent::__construct($identity); $this->user = $user; @@ -115,11 +115,11 @@ public static function from_template(StreamContext $context) return new self( $context->get_meta_by_key('user'), $context->get_current_identity(), - $context->get_meta_by_key('client_meta')->is_panel(), $context->get_optional_property('cap_desc', true), $context->get_optional_property('cap', 2), $context->get_optional_property('ranking_context', 'dashboard'), $context->get_meta_by_key('allow_ranking'), + $context->get_meta_by_key('client_meta')->is_panel() ); } diff --git a/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php b/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php index 7c6dd46..7121cf2 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php @@ -76,12 +76,12 @@ protected function setUp(): void $this->invalid_stream_elements = $this->setup_capped_invalid_stream_elements($blog_ids); $this->enabled_ranker_instance = $this->getMockBuilder(CappedPostRanker::class) - ->setConstructorArgs([$this->mock_user, $this->identity, false, true, 2, 'dashboard', false]) + ->setConstructorArgs([$this->mock_user, $this->identity, true, 2, 'dashboard', false, false]) ->getMock(); $this->enabled_ranker_instance->method('can_rank')->willReturn(true); $this->disabled_ranker_instance = $this->getMockBuilder(CappedPostRanker::class) - ->setConstructorArgs([$this->mock_user, $this->identity, false, true, 2, 'dashboard', false]) + ->setConstructorArgs([$this->mock_user, $this->identity, true, 2, 'dashboard', false, false]) ->getMock(); $this->disabled_ranker_instance->method('can_rank')->willReturn(false); } From 99439a5786d8da189924c889adb38ed3a54b5909 Mon Sep 17 00:00:00 2001 From: lucila Date: Fri, 24 Oct 2025 14:02:46 -0300 Subject: [PATCH 02/28] use assertSame instead of assertEquals --- .../StreamBuilder/StreamRankers/CappedPostRankerTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php b/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php index 7121cf2..9279a50 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php @@ -186,10 +186,10 @@ public function test_get_blog_dictionaries(): void $this->assertSame($post_id, $original_element->get_post_id()); } # Which posts belong to which blogs - $expected_blog_to_posts_ids = [111 => [1, 2, 3, 4, 5], 222 => [6, 7], 333 => [8]]; - $this->assertEquals($blog_to_posts_ids, $expected_blog_to_posts_ids); + $expected_blog_to_posts_ids = [111 => ['1', '2', '3', '4', '5'], 222 => ['6', '7'], 333 => ['8']]; + $this->assertSame($expected_blog_to_posts_ids, $blog_to_posts_ids); $expected_stats_per_blog = [111 => [5, 0], 222 => [2, 0], 333 => [1, 0]]; - $this->assertEquals($stats_per_blog, $expected_stats_per_blog); + $this->assertSame($expected_stats_per_blog, $stats_per_blog); } /** From 988aa54cff8ee7303385faa2553b785398f0585b Mon Sep 17 00:00:00 2001 From: lucila Date: Fri, 24 Oct 2025 14:29:24 -0300 Subject: [PATCH 03/28] cover mutations in CappedPostRanker --- .../StreamRankers/CappedPostRankerTest.php | 89 +++++++++++++++++-- 1 file changed, 81 insertions(+), 8 deletions(-) diff --git a/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php b/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php index 9279a50..6ed4135 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php @@ -165,7 +165,7 @@ public function test_disabled_rank(): void public function test_invalid_elements(): void { $this->expectException(TypeMismatchException::class); - $this->invokeMethod($this->enabled_ranker_instance, 'pre_fetch', $this->invalid_stream_elements); + $this->invokeMethod($this->enabled_ranker_instance, 'pre_fetch', [$this->invalid_stream_elements]); } /** @@ -176,7 +176,7 @@ public function test_get_blog_dictionaries(): void $dictionaries = $this->invokeMethod( $this->enabled_ranker_instance, 'get_blog_dictionaries', - $this->stream_elements + [$this->stream_elements] ); $post_to_element = $dictionaries[0]; $blog_to_posts_ids = $dictionaries[1]; @@ -192,22 +192,95 @@ public function test_get_blog_dictionaries(): void $this->assertSame($expected_stats_per_blog, $stats_per_blog); } + /** + * Test that invalidate_post_id properly removes posts from blog_to_posts_ids + */ + public function test_invalidate_post_id_removes_posts(): void + { + $ranker = new CappedPostRanker($this->mock_user, $this->identity, true, 2, 'dashboard', false); + + // Get initial blog dictionaries using reflection + $dictionaries = $this->invokeMethod($ranker, 'get_blog_dictionaries', [$this->stream_elements]); + $blog_to_posts_ids = $dictionaries[1]; + + // Verify initial state + $expected_initial = [111 => ['1', '2', '3', '4', '5'], 222 => ['6', '7'], 333 => ['8']]; + $expected_post_counts = [111 => [5, 0], 222 => [2, 0], 333 => [1, 0]]; + $this->assertSame($expected_initial, $blog_to_posts_ids); + $this->assertSame($expected_post_counts, $dictionaries[2]); + + // Test invalidating a specific post using reflection + $violated_post_id = $this->invokeMethod($ranker, 'invalidate_post_id', ['111', '3', &$blog_to_posts_ids]); + // The method returns null when a specific post ID is provided, but the post should be removed + $this->assertNull($violated_post_id); + + // Verify post was removed from blog 111 + $this->assertCount(4, $blog_to_posts_ids[111]); // Should have 4 posts instead of 5 + $this->assertContains('1', $blog_to_posts_ids[111]); + $this->assertContains('2', $blog_to_posts_ids[111]); + $this->assertContains('4', $blog_to_posts_ids[111]); + $this->assertContains('5', $blog_to_posts_ids[111]); + } + + /** + * Test that invalidate_post_id returns the first available post when no specific post is provided + */ + public function test_invalidate_post_id_returns_first_available(): void + { + $ranker = new CappedPostRanker($this->mock_user, $this->identity, true, 2, 'dashboard', false); + + $dictionaries = $this->invokeMethod($ranker, 'get_blog_dictionaries', [$this->stream_elements]); + $blog_to_posts_ids = $dictionaries[1]; + + // Test getting first available post from blog 111 using reflection + $first_post = $this->invokeMethod($ranker, 'invalidate_post_id', ['111', null, &$blog_to_posts_ids]); + $this->assertSame(1, $first_post); // Returns integer, not string + + // Verify first post was removed + $this->assertNotContains('1', $blog_to_posts_ids[111]); + $this->assertCount(4, $blog_to_posts_ids[111]); + $this->assertContains('2', $blog_to_posts_ids[111]); + $this->assertContains('3', $blog_to_posts_ids[111]); + $this->assertContains('4', $blog_to_posts_ids[111]); + $this->assertContains('5', $blog_to_posts_ids[111]); + } + + /** + * Test that invalidate_post_id handles non-existent posts gracefully + */ + public function test_invalidate_post_id_nonexistent_post(): void + { + $ranker = new CappedPostRanker($this->mock_user, $this->identity, true, 2, 'dashboard', false, false); + + $dictionaries = $this->invokeMethod($ranker, 'get_blog_dictionaries', [$this->stream_elements]); + $blog_to_posts_ids = $dictionaries[1]; + + // Try to invalidate a non-existent post using reflection + $result = $this->invokeMethod($ranker, 'invalidate_post_id', ['111', '999', &$blog_to_posts_ids]); + $this->assertNull($result); + + // Verify no changes were made + $this->assertCount(5, $blog_to_posts_ids[111]); + $this->assertContains('1', $blog_to_posts_ids[111]); + $this->assertContains('2', $blog_to_posts_ids[111]); + $this->assertContains('3', $blog_to_posts_ids[111]); + $this->assertContains('4', $blog_to_posts_ids[111]); + $this->assertContains('5', $blog_to_posts_ids[111]); + } + /** * Helper method to enable testing private and protected methods - * @param object $object The object that the private method belongs to + * @param CappedPostRanker $object The object that the private method belongs to * @param string $method_name The name of the method we want to test * @param array $parameters List of parameters passed to the method * @return mixed Whatever the method would return * @throws \ReflectionException Something went wrong with reflection */ - public function invokeMethod(object &$object, string $method_name, array $parameters = []) + public function invokeMethod(CappedPostRanker &$object, string $method_name, array $parameters = []) { - $reflection = new \ReflectionClass(get_class($object)); + $reflection = new \ReflectionClass(CappedPostRanker::class); $method = $reflection->getMethod($method_name); $method->setAccessible(true); - if ($method_name = 'get_blog_dictionaries') { - $parameters = [$parameters]; - } return $method->invokeArgs($object, $parameters); } } From 476a3043786a6782170d408eca26608d411610c6 Mon Sep 17 00:00:00 2001 From: lucila Date: Fri, 24 Oct 2025 14:43:35 -0300 Subject: [PATCH 04/28] coverage for mutants in Stream.php --- .../StreamBuilder/Streams/StreamTest.php | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/unit/Tumblr/StreamBuilder/Streams/StreamTest.php b/tests/unit/Tumblr/StreamBuilder/Streams/StreamTest.php index 94a30db..73e2638 100644 --- a/tests/unit/Tumblr/StreamBuilder/Streams/StreamTest.php +++ b/tests/unit/Tumblr/StreamBuilder/Streams/StreamTest.php @@ -99,4 +99,68 @@ public function test_empty_identity() ->setConstructorArgs(['']) ->getMockForAbstractClass(); } + + /** + * Test that the array_map function in enumerate method is executed + * This test verifies that the escaped mutant (removal of setComponent call) would be caught + */ + public function test_enumerate_executes_array_map_function() + { + // Create a mock stream that tracks if _enumerate was called + $stream = $this->getMockBuilder(Stream::class) + ->setConstructorArgs(['test_stream']) + ->setMethods(['_enumerate']) + ->getMockForAbstractClass(); + + // Create a mock element that tracks component setting + $mock_element = $this->getMockBuilder(\Tumblr\StreamBuilder\StreamElements\StreamElement::class) + ->disableOriginalConstructor() + ->getMock(); + + $mock_element->method('getComponent')->willReturn(null); + $mock_element->expects($this->once()) + ->method('setComponent') + ->with('test_component'); + + $stream->method('_enumerate') + ->willReturn(new \Tumblr\StreamBuilder\StreamResult(false, [$mock_element])); + + // Set the component on the stream + $stream->setComponent('test_component'); + + // Enumerate and verify the element's setComponent was called + $result = $stream->enumerate(1); + $this->assertCount(1, $result->get_elements()); + } + + /** + * Test that elements with existing components are not overridden + */ + public function test_enumerate_does_not_override_existing_component() + { + // Create a mock stream + $stream = $this->getMockBuilder(Stream::class) + ->setConstructorArgs(['test_stream']) + ->setMethods(['_enumerate']) + ->getMockForAbstractClass(); + + // Create a mock element that already has a component + $mock_element = $this->getMockBuilder(\Tumblr\StreamBuilder\StreamElements\StreamElement::class) + ->disableOriginalConstructor() + ->getMock(); + + $mock_element->method('getComponent')->willReturn('existing_component'); + $mock_element->expects($this->never()) + ->method('setComponent'); + + $stream->method('_enumerate') + ->willReturn(new \Tumblr\StreamBuilder\StreamResult(false, [$mock_element])); + + // Set a component on the stream + $stream->setComponent('test_component'); + + // Enumerate and verify setComponent was not called + $result = $stream->enumerate(1); + $this->assertCount(1, $result->get_elements()); + } } From a86e4613508dcdba4180bffb8ebbb2f384c6071c Mon Sep 17 00:00:00 2001 From: lucila Date: Fri, 24 Oct 2025 14:51:34 -0300 Subject: [PATCH 05/28] remove --only-covered option from vendor/bin/infection build step --- .github/workflows/mt.yml | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mt.yml b/.github/workflows/mt.yml index 18ea155..0b37df2 100644 --- a/.github/workflows/mt.yml +++ b/.github/workflows/mt.yml @@ -55,4 +55,4 @@ jobs: if: github.event_name == 'pull_request' run: | git fetch --depth=1 origin $GITHUB_BASE_REF - vendor/bin/infection --coverage=build/logs --threads=$(nproc) --show-mutations --no-interaction --only-covered --only-covering-test-cases --skip-initial-tests --git-diff-lines --git-diff-base=origin/$GITHUB_BASE_REF + vendor/bin/infection --coverage=build/logs --threads=$(nproc) --show-mutations --no-interaction --only-covering-test-cases --skip-initial-tests --git-diff-lines --git-diff-base=origin/$GITHUB_BASE_REF diff --git a/Makefile b/Makefile index c188a07..702a2f7 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ COMPOSER=$(PHP) $(shell which composer) INFECTION=vendor/bin/infection MIN_MSI=50 MIN_COVERED_MSI=50 -INFECTION_ARGS=--min-msi=$(MIN_MSI) --min-covered-msi=$(MIN_COVERED_MSI) --threads=$(JOBS) --coverage=build/logs --show-mutations --no-interaction --only-covered --only-covering-test-cases +INFECTION_ARGS=--min-msi=$(MIN_MSI) --min-covered-msi=$(MIN_COVERED_MSI) --threads=$(JOBS) --coverage=build/logs --show-mutations --no-interaction --only-covering-test-cases all: test From 6a12de8ff262af3ed7f2afd5501047f198ce7d13 Mon Sep 17 00:00:00 2001 From: lucila Date: Fri, 24 Oct 2025 15:14:07 -0300 Subject: [PATCH 06/28] remove mutants from StreamElementInjection --- .../StreamElementInjectionTest.php | 204 +++++++++++++++++- 1 file changed, 203 insertions(+), 1 deletion(-) diff --git a/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php b/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php index a13b364..f0b8657 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php @@ -97,4 +97,206 @@ public function test_get_element() ); return $stream_ele_injection; } -} + + /** + * Test that component is set on element when element has no component + * This test verifies that the escaped mutant (removal of setComponent call) would be caught + */ + public function test_component_is_set_when_element_has_no_component() + { + /** @var StreamInjector|\PHPUnit\Framework\MockObject\MockObject $injector */ + $injector = $this->getMockBuilder(StreamInjector::class) + ->setConstructorArgs(['awesome_injector']) + ->getMock(); + + $injector->method('getComponent') + ->willReturn('injector_component'); + + // Create a test element that tracks component setting + $element = new class('test_provider') extends StreamElement { + public $component_set_called = false; + public $component_value = null; + + public function setComponent(?string $component): void { + $this->component_set_called = true; + $this->component_value = $component; + parent::setComponent($component); + } + + public function getComponent(): ?string { + return $this->component_value; + } + + public function get_element_id(): string { + return 'test_element'; + } + + public function get_original_element(): StreamElement { + return $this; + } + + public function get_parent_element(): StreamElement { + return $this; + } + + public function get_cache_key(): string { + return 'test_cache_key'; + } + + public function add_debug_info(string $header, string $field, $value): void { + // No-op + } + + public function get_debug_info(): array { + return []; + } + + public function to_string(): string { + return 'test_element'; + } + + public static function from_template(\Tumblr\StreamBuilder\StreamContext $context) { + return new self('test_provider'); + } + }; + + new StreamElementInjection($injector, $element); + + // Verify that setComponent was called + $this->assertTrue($element->component_set_called); + $this->assertEquals('injector_component', $element->component_value); + } + + /** + * Test that component is not set when element already has a component + */ + public function test_component_is_not_set_when_element_has_existing_component() + { + /** @var StreamInjector|\PHPUnit\Framework\MockObject\MockObject $injector */ + $injector = $this->getMockBuilder(StreamInjector::class) + ->setConstructorArgs(['awesome_injector']) + ->getMock(); + + $injector->method('getComponent') + ->willReturn('injector_component'); + + // Create a test element that already has a component + $element = new class('test_provider') extends StreamElement { + public $component_set_called = false; + + public function setComponent(?string $component): void { + $this->component_set_called = true; + parent::setComponent($component); + } + + public function getComponent(): ?string { + return 'existing_component'; + } + + public function get_element_id(): string { + return 'test_element'; + } + + public function get_original_element(): StreamElement { + return $this; + } + + public function get_parent_element(): StreamElement { + return $this; + } + + public function get_cache_key(): string { + return 'test_cache_key'; + } + + public function add_debug_info(string $header, string $field, $value): void { + // No-op + } + + public function get_debug_info(): array { + return []; + } + + public function to_string(): string { + return 'test_element'; + } + + public static function from_template(\Tumblr\StreamBuilder\StreamContext $context) { + return new self('test_provider'); + } + }; + + new StreamElementInjection($injector, $element); + + // Verify that setComponent was not called + $this->assertFalse($element->component_set_called); + } + + /** + * Test that component is set when element has empty string component + */ + public function test_component_is_set_when_element_has_empty_component() + { + /** @var StreamInjector|\PHPUnit\Framework\MockObject\MockObject $injector */ + $injector = $this->getMockBuilder(StreamInjector::class) + ->setConstructorArgs(['awesome_injector']) + ->getMock(); + + $injector->method('getComponent') + ->willReturn('injector_component'); + + // Create a test element with empty component + $element = new class('test_provider') extends StreamElement { + public $component_set_called = false; + public $component_value = null; + + public function setComponent(?string $component): void { + $this->component_set_called = true; + $this->component_value = $component; + parent::setComponent($component); + } + + public function getComponent(): ?string { + return ''; + } + + public function get_element_id(): string { + return 'test_element'; + } + + public function get_original_element(): StreamElement { + return $this; + } + + public function get_parent_element(): StreamElement { + return $this; + } + + public function get_cache_key(): string { + return 'test_cache_key'; + } + + public function add_debug_info(string $header, string $field, $value): void { + // No-op + } + + public function get_debug_info(): array { + return []; + } + + public function to_string(): string { + return 'test_element'; + } + + public static function from_template(\Tumblr\StreamBuilder\StreamContext $context) { + return new self('test_provider'); + } + }; + + new StreamElementInjection($injector, $element); + + // Verify that setComponent was called + $this->assertTrue($element->component_set_called); + $this->assertEquals('injector_component', $element->component_value); + } +} \ No newline at end of file From 6d5ee8c3f2af83a979d3eea6a8d010373cec214c Mon Sep 17 00:00:00 2001 From: lucila Date: Fri, 24 Oct 2025 15:14:38 -0300 Subject: [PATCH 07/28] dont use the latest infection version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3ed12fa..da753c9 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "ext-mbstring": "*", "ergebnis/composer-normalize": "^2.8", "friendsofphp/php-cs-fixer": "^3.18", - "infection/infection": ">=0.10.5", + "infection/infection": "^0.29", "phan/phan": ">=1.1", "php-coveralls/php-coveralls": "^2.6", "php-parallel-lint/php-parallel-lint": "^1.3", From dfab092b7532fe4cfcacd9de4410415688e63b6f Mon Sep 17 00:00:00 2001 From: lucila Date: Fri, 24 Oct 2025 15:16:50 -0300 Subject: [PATCH 08/28] revert "only-covered" changes --- .github/workflows/mt.yml | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mt.yml b/.github/workflows/mt.yml index 0b37df2..18ea155 100644 --- a/.github/workflows/mt.yml +++ b/.github/workflows/mt.yml @@ -55,4 +55,4 @@ jobs: if: github.event_name == 'pull_request' run: | git fetch --depth=1 origin $GITHUB_BASE_REF - vendor/bin/infection --coverage=build/logs --threads=$(nproc) --show-mutations --no-interaction --only-covering-test-cases --skip-initial-tests --git-diff-lines --git-diff-base=origin/$GITHUB_BASE_REF + vendor/bin/infection --coverage=build/logs --threads=$(nproc) --show-mutations --no-interaction --only-covered --only-covering-test-cases --skip-initial-tests --git-diff-lines --git-diff-base=origin/$GITHUB_BASE_REF diff --git a/Makefile b/Makefile index 702a2f7..c188a07 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ COMPOSER=$(PHP) $(shell which composer) INFECTION=vendor/bin/infection MIN_MSI=50 MIN_COVERED_MSI=50 -INFECTION_ARGS=--min-msi=$(MIN_MSI) --min-covered-msi=$(MIN_COVERED_MSI) --threads=$(JOBS) --coverage=build/logs --show-mutations --no-interaction --only-covering-test-cases +INFECTION_ARGS=--min-msi=$(MIN_MSI) --min-covered-msi=$(MIN_COVERED_MSI) --threads=$(JOBS) --coverage=build/logs --show-mutations --no-interaction --only-covered --only-covering-test-cases all: test From 5ac024c0fa22cb48d2fb02f38a13b3536031a5e1 Mon Sep 17 00:00:00 2001 From: lucila Date: Fri, 24 Oct 2025 15:22:16 -0300 Subject: [PATCH 09/28] update infection version in composer to be 0.10 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index da753c9..13ff2b2 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "ext-mbstring": "*", "ergebnis/composer-normalize": "^2.8", "friendsofphp/php-cs-fixer": "^3.18", - "infection/infection": "^0.29", + "infection/infection": "^0.10", "phan/phan": ">=1.1", "php-coveralls/php-coveralls": "^2.6", "php-parallel-lint/php-parallel-lint": "^1.3", From e9b986e4a5bcd0367e529d8ea03c31423973198a Mon Sep 17 00:00:00 2001 From: lucila Date: Fri, 24 Oct 2025 20:09:29 -0300 Subject: [PATCH 10/28] restore composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 13ff2b2..3ed12fa 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "ext-mbstring": "*", "ergebnis/composer-normalize": "^2.8", "friendsofphp/php-cs-fixer": "^3.18", - "infection/infection": "^0.10", + "infection/infection": ">=0.10.5", "phan/phan": ">=1.1", "php-coveralls/php-coveralls": "^2.6", "php-parallel-lint/php-parallel-lint": "^1.3", From d359609ecfcb84d44eb20a41fcaf094673ea0211 Mon Sep 17 00:00:00 2001 From: lucila Date: Fri, 24 Oct 2025 20:18:33 -0300 Subject: [PATCH 11/28] addresss mutants for chron stream mixer --- .../Streams/ChronologicalStreamMixerTest.php | 211 +++++++++++++++++- 1 file changed, 206 insertions(+), 5 deletions(-) diff --git a/tests/unit/Tumblr/StreamBuilder/Streams/ChronologicalStreamMixerTest.php b/tests/unit/Tumblr/StreamBuilder/Streams/ChronologicalStreamMixerTest.php index 6a92a66..a1257cf 100644 --- a/tests/unit/Tumblr/StreamBuilder/Streams/ChronologicalStreamMixerTest.php +++ b/tests/unit/Tumblr/StreamBuilder/Streams/ChronologicalStreamMixerTest.php @@ -73,9 +73,9 @@ public function get_timestamp_ms(): int /** * @inheritDoc */ - public function get_cache_key() + public function get_cache_key(): string { - // TODO: Implement get_cache_key() method. + return ''; } /** @@ -83,7 +83,7 @@ public function get_cache_key() */ protected function to_string(): string { - // TODO: Implement to_string() method. + return ''; } /** @@ -91,7 +91,7 @@ protected function to_string(): string */ public function to_template(): array { - // Testing Shim + return []; } /** @@ -99,7 +99,7 @@ public function to_template(): array */ public static function from_template(StreamContext $context): self { - // Testing Shim + return new self(); } }; } @@ -326,4 +326,205 @@ public function testSingleStreamException(): void $result2 = $stream->enumerate(10); $this->assertFalse($result2->is_exhaustive()); } + + /** + * Test that pre_fetch_all is called on elements during mixing + * This test verifies that the escaped mutant (removal of pre_fetch_all call) would be caught + */ + public function test_pre_fetch_all_is_called_on_elements(): void + { + // Create a test element that tracks pre_fetch calls + $test_element = new class('test_provider', null, 1400000000000) extends LeafStreamElement implements ChronologicalStreamElement { + public $pre_fetch_called = false; + private $ts; + + public function __construct(string $provider_identity, $cursor, int $ts) + { + parent::__construct($provider_identity, $cursor); + $this->ts = $ts; + } + + public function get_timestamp_ms(): int + { + return $this->ts; + } + + public static function pre_fetch(array $elements): void + { + // Mark that pre_fetch was called by setting a static property + // We'll use a simple approach to track this + foreach ($elements as $element) { + if ($element instanceof self) { + $element->pre_fetch_called = true; + } + } + } + + public function get_cache_key(): string + { + return 'test_cache_key'; + } + + protected function to_string(): string + { + return 'test_element'; + } + + public function to_template(): array + { + return []; + } + + public static function from_template(StreamContext $context): self + { + return new self('test_provider', null, 1400000000000); + } + }; + + // Create a mock stream that returns our test element + $stream = $this->getMockBuilder(Stream::class) + ->setConstructorArgs(['test_stream']) + ->getMockForAbstractClass(); + + $stream->method('_enumerate') + ->willReturn(new StreamResult(false, [$test_element])); + + $mixer = new ChronologicalStreamMixer( + new NoopInjector('test_injector'), + 'test_mixer', + [$stream], + QUERY_SORT_DESC + ); + + // Enumerate to trigger the mixing process + $result = $mixer->enumerate(1); + + // Verify that pre_fetch was called on the element + $this->assertTrue($test_element->pre_fetch_called); + $this->assertCount(1, $result->get_elements()); + } + + /** + * Test that pre_fetch_all is called on multiple elements during mixing + */ + public function test_pre_fetch_all_is_called_on_multiple_elements(): void + { + // Create test elements that track pre_fetch calls + $element1 = new class('test_provider1', null, 1400000000000) extends LeafStreamElement implements ChronologicalStreamElement { + public $pre_fetch_called = false; + private $ts; + + public function __construct(string $provider_identity, $cursor, int $ts) + { + parent::__construct($provider_identity, $cursor); + $this->ts = $ts; + } + + public function get_timestamp_ms(): int + { + return $this->ts; + } + + public static function pre_fetch(array $elements): void + { + // Mark that pre_fetch was called by setting a static property + // We'll use a simple approach to track this + foreach ($elements as $element) { + if ($element instanceof self) { + $element->pre_fetch_called = true; + } + } + } + + public function get_cache_key(): string + { + return 'test_cache_key1'; + } + + protected function to_string(): string + { + return 'test_element1'; + } + + public function to_template(): array + { + return []; + } + + public static function from_template(StreamContext $context): self + { + return new self('test_provider1', null, 1400000000000); + } + }; + + $element2 = new class('test_provider2', null, 1400000000001) extends LeafStreamElement implements ChronologicalStreamElement { + public $pre_fetch_called = false; + private $ts; + + public function __construct(string $provider_identity, $cursor, int $ts) + { + parent::__construct($provider_identity, $cursor); + $this->ts = $ts; + } + + public function get_timestamp_ms(): int + { + return $this->ts; + } + + public static function pre_fetch(array $elements): void + { + // Mark that pre_fetch was called by setting a static property + // We'll use a simple approach to track this + foreach ($elements as $element) { + if ($element instanceof self) { + $element->pre_fetch_called = true; + } + } + } + + public function get_cache_key(): string + { + return 'test_cache_key2'; + } + + protected function to_string(): string + { + return 'test_element2'; + } + + public function to_template(): array + { + return []; + } + + public static function from_template(StreamContext $context): self + { + return new self('test_provider2', null, 1400000000001); + } + }; + + // Create a mock stream that returns our test elements + $stream = $this->getMockBuilder(Stream::class) + ->setConstructorArgs(['test_stream']) + ->getMockForAbstractClass(); + + $stream->method('_enumerate') + ->willReturn(new StreamResult(false, [$element1, $element2])); + + $mixer = new ChronologicalStreamMixer( + new NoopInjector('test_injector'), + 'test_mixer', + [$stream], + QUERY_SORT_DESC + ); + + // Enumerate to trigger the mixing process + $result = $mixer->enumerate(2); + + // Verify that pre_fetch was called on both elements + $this->assertTrue($element1->pre_fetch_called); + $this->assertTrue($element2->pre_fetch_called); + $this->assertCount(2, $result->get_elements()); + } } From ac0ca26a24a33d61bef002ae895528877d065ec9 Mon Sep 17 00:00:00 2001 From: lucila Date: Fri, 24 Oct 2025 20:21:01 -0300 Subject: [PATCH 12/28] fix mutants in stream serializer --- .../StreamBuilder/StreamSerializerTest.php | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/unit/Tumblr/StreamBuilder/StreamSerializerTest.php b/tests/unit/Tumblr/StreamBuilder/StreamSerializerTest.php index 549553f..a53679a 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamSerializerTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamSerializerTest.php @@ -129,4 +129,73 @@ public function testFromTemplateWithSkippedComponents(): void $this->assertFalse($filtered_stream->isSkippedComponent()); $this->assertTrue($filtered_stream->getInner()->isSkippedComponent()); } + + /** + * Test that component is set on object during deserialization + * This test verifies that the escaped mutant (removal of setComponent call) would be caught + */ + public function test_component_is_set_on_object_during_deserialization(): void + { + // Use a real class that can be instantiated + $template = [ + '_type' => NullStream::class, + StreamContext::COMPONENT_NAME => 'test_component' + ]; + + $context = new StreamContext($template, []); + $result = StreamSerializer::from_template($context); + + // Verify that the component was set + $this->assertEquals('test_component', $result->getComponent()); + } + + /** + * Test that component is not set when no component is specified in template + */ + public function test_component_is_not_set_when_no_component_specified(): void + { + $template = [ + '_type' => NullStream::class + ]; + + $context = new StreamContext($template, []); + $result = StreamSerializer::from_template($context); + + // Verify that no component was set + $this->assertNull($result->getComponent()); + } + + /** + * Test that component is set to null when component is explicitly null + */ + public function test_component_is_set_to_null_when_explicitly_null(): void + { + $template = [ + '_type' => NullStream::class, + StreamContext::COMPONENT_NAME => null + ]; + + $context = new StreamContext($template, []); + $result = StreamSerializer::from_template($context); + + // Verify that component was set to null + $this->assertNull($result->getComponent()); + } + + /** + * Test that component is set to empty string when component is empty string + */ + public function test_component_is_set_to_empty_string_when_empty(): void + { + $template = [ + '_type' => NullStream::class, + StreamContext::COMPONENT_NAME => '' + ]; + + $context = new StreamContext($template, []); + $result = StreamSerializer::from_template($context); + + // Verify that component was set to empty string + $this->assertEquals('', $result->getComponent()); + } } From 599a9b0ab915e4dcc2dc3d49d0fe11d029c08e64 Mon Sep 17 00:00:00 2001 From: lucila Date: Sat, 25 Oct 2025 01:10:31 -0300 Subject: [PATCH 13/28] fix some mutants --- .../ChronologicalRangeFilterTest.php | 110 +++++++++ .../StreamFilters/StreamElementFilterTest.php | 217 ++++++++++++++++ .../GeneralStreamInjectorTest.php | 41 +++ .../StreamRankers/CappedPostRankerTest.php | 101 ++++++++ .../StreamRankers/StreamRankerTest.php | 233 ++++++++++++++++++ 5 files changed, 702 insertions(+) create mode 100644 tests/unit/Tumblr/StreamBuilder/StreamFilters/StreamElementFilterTest.php create mode 100644 tests/unit/Tumblr/StreamBuilder/StreamRankers/StreamRankerTest.php diff --git a/tests/unit/Tumblr/StreamBuilder/StreamFilters/ChronologicalRangeFilterTest.php b/tests/unit/Tumblr/StreamBuilder/StreamFilters/ChronologicalRangeFilterTest.php index 141df9a..d3f7fa6 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamFilters/ChronologicalRangeFilterTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamFilters/ChronologicalRangeFilterTest.php @@ -172,6 +172,116 @@ private function get_mock_nonchrono_elem() ->getMockForAbstractClass(); } + /** + * Test that pre_fetch_all is called on elements during filtering + * This test verifies that the escaped mutant (removal of pre_fetch_all call) would be caught + */ + public function test_pre_fetch_all_is_called_on_elements(): void + { + // Create test elements that track pre_fetch calls + $test_element1 = new class(1000) extends LeafStreamElement implements ChronologicalStreamElement { + public $pre_fetch_called = false; + private $ts; + + public function __construct(int $timestamp_ms) + { + parent::__construct('test_provider', null); + $this->ts = $timestamp_ms; + } + + public function get_timestamp_ms(): int + { + return $this->ts; + } + + public static function pre_fetch(array $elements): void + { + // Mark that pre_fetch was called by setting a static property + foreach ($elements as $element) { + if ($element instanceof self) { + $element->pre_fetch_called = true; + } + } + } + + public function get_cache_key(): string + { + return 'test_cache_key1'; + } + + protected function to_string(): string + { + return 'test_element1'; + } + + public function to_template(): array + { + return []; + } + + public static function from_template(StreamContext $context): self + { + return new self(1000); + } + }; + + $test_element2 = new class(2000) extends LeafStreamElement implements ChronologicalStreamElement { + public $pre_fetch_called = false; + private $ts; + + public function __construct(int $timestamp_ms) + { + parent::__construct('test_provider', null); + $this->ts = $timestamp_ms; + } + + public function get_timestamp_ms(): int + { + return $this->ts; + } + + public static function pre_fetch(array $elements): void + { + // Mark that pre_fetch was called by setting a static property + foreach ($elements as $element) { + if ($element instanceof self) { + $element->pre_fetch_called = true; + } + } + } + + public function get_cache_key(): string + { + return 'test_cache_key2'; + } + + protected function to_string(): string + { + return 'test_element2'; + } + + public function to_template(): array + { + return []; + } + + public static function from_template(StreamContext $context): self + { + return new self(2000); + } + }; + + $elements = [$test_element1, $test_element2]; + $filter = new ChronologicalRangeFilter('test_filter', 3000, 500, true); + + // Call filter to trigger pre_fetch + $result = $filter->filter($elements); + + // Verify that pre_fetch was called on both elements + $this->assertTrue($test_element1->pre_fetch_called); + $this->assertTrue($test_element2->pre_fetch_called); + } + /** * @param int $timestamp_ms The timestamp. * @return LeafStreamElement|ChronologicalStreamElement diff --git a/tests/unit/Tumblr/StreamBuilder/StreamFilters/StreamElementFilterTest.php b/tests/unit/Tumblr/StreamBuilder/StreamFilters/StreamElementFilterTest.php new file mode 100644 index 0000000..9eaf049 --- /dev/null +++ b/tests/unit/Tumblr/StreamBuilder/StreamFilters/StreamElementFilterTest.php @@ -0,0 +1,217 @@ +pre_fetch_called = true; + $this->pre_fetch_elements = $elements; + } + + protected function should_release(StreamElement $e): bool + { + // Always retain elements for this test + return false; + } + + public function get_cache_key(): ?string + { + return 'test_filter_cache_key'; + } + + public function to_template(): array + { + return ['_type' => 'TestFilter']; + } + + public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self + { + return new self('test_filter'); + } + }; + + // Create test elements + $element1 = $this->getMockBuilder(StreamElement::class) + ->setConstructorArgs(['test1']) + ->getMockForAbstractClass(); + $element2 = $this->getMockBuilder(StreamElement::class) + ->setConstructorArgs(['test2']) + ->getMockForAbstractClass(); + + $elements = [$element1, $element2]; + + // Call filter method + $result = $test_filter->filter($elements); + + // Verify that pre_fetch was called + $this->assertTrue($test_filter->pre_fetch_called); + $this->assertSame($elements, $test_filter->pre_fetch_elements); + $this->assertSame($elements, $result->get_retained()); + $this->assertEmpty($result->get_released()); + } + + /** + * Test that pre_fetch is called with multiple elements + */ + public function test_pre_fetch_is_called_with_multiple_elements(): void + { + // Create a test filter that tracks pre_fetch calls + $test_filter = new class('test_filter') extends StreamElementFilter { + public $pre_fetch_called = false; + public $pre_fetch_elements = []; + + protected function pre_fetch(array $elements): void + { + $this->pre_fetch_called = true; + $this->pre_fetch_elements = $elements; + } + + protected function should_release(StreamElement $e): bool + { + // Always retain elements for this test + return false; + } + + public function get_cache_key(): ?string + { + return 'test_filter_cache_key'; + } + + public function to_template(): array + { + return ['_type' => 'TestFilter']; + } + + public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self + { + return new self('test_filter'); + } + }; + + // Create multiple test elements + $elements = []; + for ($i = 1; $i <= 5; $i++) { + $elements[] = $this->getMockBuilder(StreamElement::class) + ->setConstructorArgs(["test{$i}"]) + ->getMockForAbstractClass(); + } + + // Call filter method + $result = $test_filter->filter($elements); + + // Verify that pre_fetch was called with all elements + $this->assertTrue($test_filter->pre_fetch_called); + $this->assertSame($elements, $test_filter->pre_fetch_elements); + $this->assertSame($elements, $result->get_retained()); + $this->assertEmpty($result->get_released()); + $this->assertCount(5, $test_filter->pre_fetch_elements); + } + + /** + * Test that pre_fetch is called even when some elements are released + */ + public function test_pre_fetch_is_called_even_when_elements_are_released(): void + { + // Create a test filter that tracks pre_fetch calls + $test_filter = new class('test_filter') extends StreamElementFilter { + public $pre_fetch_called = false; + public $pre_fetch_elements = []; + + protected function pre_fetch(array $elements): void + { + $this->pre_fetch_called = true; + $this->pre_fetch_elements = $elements; + } + + protected function should_release(StreamElement $e): bool + { + // Release elements with '2' or '4' in their identity + return (strpos($e->get_element_id(), '2') !== false || strpos($e->get_element_id(), '4') !== false); + } + + public function get_cache_key(): ?string + { + return 'test_filter_cache_key'; + } + + public function to_template(): array + { + return ['_type' => 'TestFilter']; + } + + public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self + { + return new self('test_filter'); + } + }; + + // Create test elements + $element1 = $this->getMockBuilder(StreamElement::class) + ->setConstructorArgs(['test1']) + ->getMockForAbstractClass(); + $element2 = $this->getMockBuilder(StreamElement::class) + ->setConstructorArgs(['test2']) + ->getMockForAbstractClass(); + $element3 = $this->getMockBuilder(StreamElement::class) + ->setConstructorArgs(['test3']) + ->getMockForAbstractClass(); + $element4 = $this->getMockBuilder(StreamElement::class) + ->setConstructorArgs(['test4']) + ->getMockForAbstractClass(); + + // Mock the get_element_id method to return the identity + $element1->method('get_element_id')->willReturn('test1'); + $element2->method('get_element_id')->willReturn('test2'); + $element3->method('get_element_id')->willReturn('test3'); + $element4->method('get_element_id')->willReturn('test4'); + + $elements = [$element1, $element2, $element3, $element4]; + + // Call filter method + $result = $test_filter->filter($elements); + + // Verify that pre_fetch was called + $this->assertTrue($test_filter->pre_fetch_called); + $this->assertSame($elements, $test_filter->pre_fetch_elements); + $this->assertCount(2, $result->get_retained()); + $this->assertCount(2, $result->get_released()); + } +} diff --git a/tests/unit/Tumblr/StreamBuilder/StreamInjectors/GeneralStreamInjectorTest.php b/tests/unit/Tumblr/StreamBuilder/StreamInjectors/GeneralStreamInjectorTest.php index fcc73c1..0e4992f 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamInjectors/GeneralStreamInjectorTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamInjectors/GeneralStreamInjectorTest.php @@ -137,6 +137,47 @@ public function testPagination(): void $this->assertSame(444, $ele->get_post_id()); } + /** + * Test that set_cursor is called on elements during injection planning + * This test verifies that the escaped mutant (removal of set_cursor call) would be caught + */ + public function test_set_cursor_is_called_on_elements(): void + { + // Create a real element that we can check the cursor state of + $test_element = new MockedPostRefElement(999, 123); + + // Set a non-null cursor initially + $test_element->set_cursor($this->getMockBuilder(StreamCursor::class)->setConstructorArgs(['test_cursor'])->getMock()); + + // Create a mock stream that returns our test element + $stream = $this->getMockBuilder(Stream::class) + ->setConstructorArgs(['test_stream']) + ->getMock(); + $stream->method('_enumerate')->willReturn(new StreamResult(false, [$test_element])); + $stream->method('to_template')->willReturn(['_type' => 'test_stream']); + + // Create injector with the test stream + $injector = new GeneralStreamInjector( + $stream, + new GlobalFixedInjectionAllocator([1]), + 'test_injector' + ); + + // Create a mock main stream + $main_stream = $this->getMockBuilder(Stream::class) + ->setConstructorArgs(['main_stream']) + ->getMock(); + + // Call plan_injection to trigger the set_cursor call + $injection_res = $injector->plan_injection(2, $main_stream); + + // Verify that the injection plan is created + $this->assertInstanceOf(\Tumblr\StreamBuilder\InjectionPlan::class, $injection_res); + + // Verify that the element's cursor was set to null + $this->assertNull($test_element->get_cursor()); + } + /** * @return void */ diff --git a/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php b/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php index 6ed4135..7cdfaeb 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php @@ -268,6 +268,107 @@ public function test_invalidate_post_id_nonexistent_post(): void $this->assertContains('5', $blog_to_posts_ids[111]); } + /** + * Test that invalidate_post_id is called during ranking by tracking method calls + * This test verifies that the escaped mutant (removal of invalidate_post_id call on line 195) would be caught + */ + public function test_invalidate_post_id_called_during_ranking(): void + { + // Create elements from the same blog to force violations with cap=1 + $blog_id = 1000; // Use integer blog ID as expected by MockedPostRefElement + $elements = []; + for ($i = 1; $i <= 3; $i++) { + $post_ref = new MockedPostRefElement($i, $blog_id); + $elements[] = new DerivedStreamElement($post_ref, 'test_provider'); + } + + // Create a test ranker that tracks invalidate_post_id calls with cap=1 to force violations + $test_ranker = new class($this->mock_user, $this->identity, true, 1, 'dashboard', false, false) extends CappedPostRanker { + public $invalidate_post_id_called = false; + public $invalidate_post_id_calls = []; + public $debug_info = []; + + protected function rank_inner(array $stream_elements, ?\Tumblr\StreamBuilder\StreamTracers\StreamTracer $tracer = null): array + { + $this->debug_info[] = "rank_inner called with " . count($stream_elements) . " elements"; + $this->debug_info[] = "can_rank() = " . ($this->can_rank() ? 'true' : 'false'); + + // Call the parent method and capture the result + $result = parent::rank_inner($stream_elements, $tracer); + + $this->debug_info[] = "rank_inner result count = " . count($result); + + // Check if the result is the same as input (no ranking applied) + if ($result === $stream_elements) { + $this->debug_info[] = "No ranking applied - result is same as input"; + } else { + $this->debug_info[] = "Ranking was applied - result differs from input"; + } + + return $result; + } + + protected function invalidate_post_id(string $blog_id, ?string $post_id, array &$blog_to_posts_ids): ?int + { + $this->invalidate_post_id_called = true; + $this->invalidate_post_id_calls[] = ['blog_id' => $blog_id, 'post_id' => $post_id]; + $this->debug_info[] = "invalidate_post_id called with blog_id: $blog_id, post_id: " . ($post_id ?? 'null'); + $this->debug_info[] = "blog_to_posts_ids before: " . json_encode($blog_to_posts_ids); + $result = parent::invalidate_post_id($blog_id, $post_id, $blog_to_posts_ids); + $this->debug_info[] = "blog_to_posts_ids after: " . json_encode($blog_to_posts_ids); + $this->debug_info[] = "invalidate_post_id result: " . ($result ?? 'null'); + return $result; + } + }; + + // Call rank method + $result = $test_ranker->rank($elements); + + // Debug output + if (!$test_ranker->invalidate_post_id_called) { + echo "Debug: can_rank() = " . ($test_ranker->can_rank() ? 'true' : 'false') . "\n"; + echo "Debug: result count = " . count($result) . "\n"; + echo "Debug: invalidate_post_id_calls = " . count($test_ranker->invalidate_post_id_calls) . "\n"; + echo "Debug info:\n"; + foreach ($test_ranker->debug_info as $info) { + echo " $info\n"; + } + } + + // Verify that invalidate_post_id was called + $this->assertTrue($test_ranker->invalidate_post_id_called, 'invalidate_post_id should have been called during ranking'); + $this->assertGreaterThan(0, count($test_ranker->invalidate_post_id_calls), 'invalidate_post_id should have been called at least once'); + $this->assertCount(count($elements), $result); + } + + /** + * Test that invalidate_post_id is called during ranking with violations by tracking method calls + * This test verifies that the escaped mutant (removal of invalidate_post_id call on line 224) would be caught + */ + public function test_invalidate_post_id_called_during_ranking_with_violations(): void + { + // Create a test ranker that tracks invalidate_post_id calls with cap=1 to force violations + $test_ranker = new class($this->mock_user, $this->identity, true, 1, 'dashboard', false, false) extends CappedPostRanker { + public $invalidate_post_id_called = false; + public $invalidate_post_id_calls = []; + + protected function invalidate_post_id(string $blog_id, ?string $post_id, array &$blog_to_posts_ids): ?int + { + $this->invalidate_post_id_called = true; + $this->invalidate_post_id_calls[] = ['blog_id' => $blog_id, 'post_id' => $post_id]; + return parent::invalidate_post_id($blog_id, $post_id, $blog_to_posts_ids); + } + }; + + // Call rank method + $result = $test_ranker->rank($this->stream_elements); + + // Verify that invalidate_post_id was called + $this->assertTrue($test_ranker->invalidate_post_id_called, 'invalidate_post_id should have been called during ranking with violations'); + $this->assertGreaterThan(0, count($test_ranker->invalidate_post_id_calls), 'invalidate_post_id should have been called at least once'); + $this->assertCount(count($this->stream_elements), $result); + } + /** * Helper method to enable testing private and protected methods * @param CappedPostRanker $object The object that the private method belongs to diff --git a/tests/unit/Tumblr/StreamBuilder/StreamRankers/StreamRankerTest.php b/tests/unit/Tumblr/StreamBuilder/StreamRankers/StreamRankerTest.php new file mode 100644 index 0000000..3dac880 --- /dev/null +++ b/tests/unit/Tumblr/StreamBuilder/StreamRankers/StreamRankerTest.php @@ -0,0 +1,233 @@ +pre_fetch_called = true; + $this->pre_fetch_elements = $elements; + } + + protected function rank_inner(array $stream_elements, ?StreamTracer $tracer = null): array + { + // Just return the elements in the same order + return $stream_elements; + } + + public function to_template(): array + { + return ['_type' => 'TestRanker']; + } + + public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self + { + return new self('test_ranker'); + } + }; + + // Create test elements + $element1 = new MockedPostRefElement(1, 1); + $element2 = new MockedPostRefElement(2, 2); + $elements = [$element1, $element2]; + + // Call rank method + $result = $test_ranker->rank($elements); + + // Verify that pre_fetch was called + $this->assertTrue($test_ranker->pre_fetch_called); + $this->assertSame($elements, $test_ranker->pre_fetch_elements); + $this->assertSame($elements, $result); + } + + /** + * Test that pre_fetch is called with tracer during ranking + */ + public function test_pre_fetch_is_called_with_tracer_during_ranking(): void + { + // Create a test ranker that tracks pre_fetch calls + $test_ranker = new class('test_ranker') extends StreamRanker { + public $pre_fetch_called = false; + public $pre_fetch_elements = []; + + protected function pre_fetch(array $elements): void + { + $this->pre_fetch_called = true; + $this->pre_fetch_elements = $elements; + } + + protected function rank_inner(array $stream_elements, ?StreamTracer $tracer = null): array + { + // Just return the elements in the same order + return $stream_elements; + } + + public function to_template(): array + { + return ['_type' => 'TestRanker']; + } + + public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self + { + return new self('test_ranker'); + } + }; + + // Create a mock tracer + $tracer = $this->getMockBuilder(StreamTracer::class) + ->disableOriginalConstructor() + ->getMock(); + + // Create test elements + $element1 = new MockedPostRefElement(1, 1); + $element2 = new MockedPostRefElement(2, 2); + $elements = [$element1, $element2]; + + // Call rank method with tracer + $result = $test_ranker->rank($elements, $tracer); + + // Verify that pre_fetch was called + $this->assertTrue($test_ranker->pre_fetch_called); + $this->assertSame($elements, $test_ranker->pre_fetch_elements); + $this->assertSame($elements, $result); + } + + /** + * Test that pre_fetch is called even when rank_inner throws an exception + */ + public function test_pre_fetch_is_called_even_when_rank_inner_throws_exception(): void + { + // Create a test ranker that tracks pre_fetch calls and throws exception + $test_ranker = new class('test_ranker') extends StreamRanker { + public $pre_fetch_called = false; + public $pre_fetch_elements = []; + + protected function pre_fetch(array $elements): void + { + $this->pre_fetch_called = true; + $this->pre_fetch_elements = $elements; + } + + protected function rank_inner(array $stream_elements, ?StreamTracer $tracer = null): array + { + // Throw an exception to test that pre_fetch is still called + throw new \Exception('Test exception'); + } + + public function to_template(): array + { + return ['_type' => 'TestRanker']; + } + + public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self + { + return new self('test_ranker'); + } + }; + + // Create test elements + $element1 = new MockedPostRefElement(1, 1); + $element2 = new MockedPostRefElement(2, 2); + $elements = [$element1, $element2]; + + // Expect an exception to be thrown + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Test exception'); + + try { + $test_ranker->rank($elements); + } finally { + // Verify that pre_fetch was called even though an exception was thrown + $this->assertTrue($test_ranker->pre_fetch_called); + $this->assertSame($elements, $test_ranker->pre_fetch_elements); + } + } + + /** + * Test that pre_fetch is called with multiple elements + */ + public function test_pre_fetch_is_called_with_multiple_elements(): void + { + // Create a test ranker that tracks pre_fetch calls + $test_ranker = new class('test_ranker') extends StreamRanker { + public $pre_fetch_called = false; + public $pre_fetch_elements = []; + + protected function pre_fetch(array $elements): void + { + $this->pre_fetch_called = true; + $this->pre_fetch_elements = $elements; + } + + protected function rank_inner(array $stream_elements, ?StreamTracer $tracer = null): array + { + // Just return the elements in the same order + return $stream_elements; + } + + public function to_template(): array + { + return ['_type' => 'TestRanker']; + } + + public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self + { + return new self('test_ranker'); + } + }; + + // Create multiple test elements + $elements = []; + for ($i = 1; $i <= 5; $i++) { + $elements[] = new MockedPostRefElement($i, $i); + } + + // Call rank method + $result = $test_ranker->rank($elements); + + // Verify that pre_fetch was called with all elements + $this->assertTrue($test_ranker->pre_fetch_called); + $this->assertSame($elements, $test_ranker->pre_fetch_elements); + $this->assertSame($elements, $result); + $this->assertCount(5, $test_ranker->pre_fetch_elements); + } +} From b2c1cb3577bb4d5cb30b237ac065a1d275c15e0f Mon Sep 17 00:00:00 2001 From: lucila Date: Mon, 27 Oct 2025 09:50:27 -0300 Subject: [PATCH 14/28] set infection version to 0.30.3 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3ed12fa..b14ff1d 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "ext-mbstring": "*", "ergebnis/composer-normalize": "^2.8", "friendsofphp/php-cs-fixer": "^3.18", - "infection/infection": ">=0.10.5", + "infection/infection": "0.30.3", "phan/phan": ">=1.1", "php-coveralls/php-coveralls": "^2.6", "php-parallel-lint/php-parallel-lint": "^1.3", From 811cf24f7dae1cf60352d1ba2db965e5eab1710e Mon Sep 17 00:00:00 2001 From: lucila Date: Mon, 27 Oct 2025 09:53:23 -0300 Subject: [PATCH 15/28] undo composer.json changes --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b14ff1d..3ed12fa 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "ext-mbstring": "*", "ergebnis/composer-normalize": "^2.8", "friendsofphp/php-cs-fixer": "^3.18", - "infection/infection": "0.30.3", + "infection/infection": ">=0.10.5", "phan/phan": ">=1.1", "php-coveralls/php-coveralls": "^2.6", "php-parallel-lint/php-parallel-lint": "^1.3", From 4db447ee0400acff7da0f05e92ebd30cdcdc5bfb Mon Sep 17 00:00:00 2001 From: lucila Date: Mon, 27 Oct 2025 10:11:59 -0300 Subject: [PATCH 16/28] phpcs errros --- .../StreamRankers/StreamRankerTest.php | 74 ++++++++++++- .../StreamBuilder/StreamSerializerTest.php | 12 +-- .../Streams/ChronologicalStreamMixerTest.php | 101 ++++++++++++++++-- 3 files changed, 173 insertions(+), 14 deletions(-) diff --git a/tests/unit/Tumblr/StreamBuilder/StreamRankers/StreamRankerTest.php b/tests/unit/Tumblr/StreamBuilder/StreamRankers/StreamRankerTest.php index 3dac880..728530d 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamRankers/StreamRankerTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamRankers/StreamRankerTest.php @@ -22,7 +22,6 @@ namespace Tests\Unit\Tumblr\StreamBuilder\StreamRankers; use Test\Mock\Tumblr\StreamBuilder\StreamElements\MockedPostRefElement; -use Tumblr\StreamBuilder\StreamElements\StreamElement; use Tumblr\StreamBuilder\StreamRankers\StreamRanker; use Tumblr\StreamBuilder\StreamTracers\StreamTracer; @@ -39,26 +38,44 @@ public function test_pre_fetch_is_called_during_ranking(): void { // Create a test ranker that tracks pre_fetch calls $test_ranker = new class('test_ranker') extends StreamRanker { + /** @var bool */ public $pre_fetch_called = false; + /** @var array */ public $pre_fetch_elements = []; + /** + * @param array $elements The elements to pre-fetch + * @return void + */ protected function pre_fetch(array $elements): void { $this->pre_fetch_called = true; $this->pre_fetch_elements = $elements; } + /** + * @param array $stream_elements The stream elements to rank + * @param StreamTracer|null $tracer Optional tracer for debugging + * @return array The ranked elements + */ protected function rank_inner(array $stream_elements, ?StreamTracer $tracer = null): array { // Just return the elements in the same order return $stream_elements; } + /** + * @return array The template array + */ public function to_template(): array { return ['_type' => 'TestRanker']; } + /** + * @param \Tumblr\StreamBuilder\StreamContext $context The stream context + * @return self The created instance + */ public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self { return new self('test_ranker'); @@ -86,26 +103,44 @@ public function test_pre_fetch_is_called_with_tracer_during_ranking(): void { // Create a test ranker that tracks pre_fetch calls $test_ranker = new class('test_ranker') extends StreamRanker { + /** @var bool */ public $pre_fetch_called = false; + /** @var array */ public $pre_fetch_elements = []; + /** + * @param array $elements The elements to pre-fetch + * @return void + */ protected function pre_fetch(array $elements): void { $this->pre_fetch_called = true; $this->pre_fetch_elements = $elements; } + /** + * @param array $stream_elements The stream elements to rank + * @param StreamTracer|null $tracer Optional tracer for debugging + * @return array The ranked elements + */ protected function rank_inner(array $stream_elements, ?StreamTracer $tracer = null): array { // Just return the elements in the same order return $stream_elements; } + /** + * @return array The template array + */ public function to_template(): array { return ['_type' => 'TestRanker']; } + /** + * @param \Tumblr\StreamBuilder\StreamContext $context The stream context + * @return self The created instance + */ public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self { return new self('test_ranker'); @@ -138,26 +173,45 @@ public function test_pre_fetch_is_called_even_when_rank_inner_throws_exception() { // Create a test ranker that tracks pre_fetch calls and throws exception $test_ranker = new class('test_ranker') extends StreamRanker { + /** @var bool */ public $pre_fetch_called = false; + /** @var array */ public $pre_fetch_elements = []; + /** + * @param array $elements The elements to pre-fetch + * @return void + */ protected function pre_fetch(array $elements): void { $this->pre_fetch_called = true; $this->pre_fetch_elements = $elements; } + /** + * @param array $stream_elements The stream elements to rank + * @param StreamTracer|null $tracer Optional tracer for debugging + * @return never Always throws exception + * @throws \Exception Always throws test exception + */ protected function rank_inner(array $stream_elements, ?StreamTracer $tracer = null): array { // Throw an exception to test that pre_fetch is still called throw new \Exception('Test exception'); } + /** + * @return array The template array + */ public function to_template(): array { return ['_type' => 'TestRanker']; } + /** + * @param \Tumblr\StreamBuilder\StreamContext $context The stream context + * @return self The created instance + */ public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self { return new self('test_ranker'); @@ -189,26 +243,44 @@ public function test_pre_fetch_is_called_with_multiple_elements(): void { // Create a test ranker that tracks pre_fetch calls $test_ranker = new class('test_ranker') extends StreamRanker { + /** @var bool */ public $pre_fetch_called = false; + /** @var array */ public $pre_fetch_elements = []; + /** + * @param array $elements The elements to pre-fetch + * @return void + */ protected function pre_fetch(array $elements): void { $this->pre_fetch_called = true; $this->pre_fetch_elements = $elements; } + /** + * @param array $stream_elements The stream elements to rank + * @param StreamTracer|null $tracer Optional tracer for debugging + * @return array The ranked elements + */ protected function rank_inner(array $stream_elements, ?StreamTracer $tracer = null): array { // Just return the elements in the same order return $stream_elements; } + /** + * @return array The template array + */ public function to_template(): array { return ['_type' => 'TestRanker']; } + /** + * @param \Tumblr\StreamBuilder\StreamContext $context The stream context + * @return self The created instance + */ public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self { return new self('test_ranker'); diff --git a/tests/unit/Tumblr/StreamBuilder/StreamSerializerTest.php b/tests/unit/Tumblr/StreamBuilder/StreamSerializerTest.php index a53679a..4fe9c84 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamSerializerTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamSerializerTest.php @@ -139,14 +139,14 @@ public function test_component_is_set_on_object_during_deserialization(): void // Use a real class that can be instantiated $template = [ '_type' => NullStream::class, - StreamContext::COMPONENT_NAME => 'test_component' + StreamContext::COMPONENT_NAME => 'test_component', ]; $context = new StreamContext($template, []); $result = StreamSerializer::from_template($context); // Verify that the component was set - $this->assertEquals('test_component', $result->getComponent()); + $this->assertSame('test_component', $result->getComponent()); } /** @@ -155,7 +155,7 @@ public function test_component_is_set_on_object_during_deserialization(): void public function test_component_is_not_set_when_no_component_specified(): void { $template = [ - '_type' => NullStream::class + '_type' => NullStream::class, ]; $context = new StreamContext($template, []); @@ -172,7 +172,7 @@ public function test_component_is_set_to_null_when_explicitly_null(): void { $template = [ '_type' => NullStream::class, - StreamContext::COMPONENT_NAME => null + StreamContext::COMPONENT_NAME => null, ]; $context = new StreamContext($template, []); @@ -189,13 +189,13 @@ public function test_component_is_set_to_empty_string_when_empty(): void { $template = [ '_type' => NullStream::class, - StreamContext::COMPONENT_NAME => '' + StreamContext::COMPONENT_NAME => '', ]; $context = new StreamContext($template, []); $result = StreamSerializer::from_template($context); // Verify that component was set to empty string - $this->assertEquals('', $result->getComponent()); + $this->assertSame('', $result->getComponent()); } } diff --git a/tests/unit/Tumblr/StreamBuilder/Streams/ChronologicalStreamMixerTest.php b/tests/unit/Tumblr/StreamBuilder/Streams/ChronologicalStreamMixerTest.php index a1257cf..890e7de 100644 --- a/tests/unit/Tumblr/StreamBuilder/Streams/ChronologicalStreamMixerTest.php +++ b/tests/unit/Tumblr/StreamBuilder/Streams/ChronologicalStreamMixerTest.php @@ -23,6 +23,7 @@ use Test\Mock\Tumblr\StreamBuilder\StreamElements\MockedPostRefElement; use Tumblr\StreamBuilder\StreamContext; +use Tumblr\StreamBuilder\StreamCursors\StreamCursor; use Tumblr\StreamBuilder\StreamElements\ChronologicalStreamElement; use Tumblr\StreamBuilder\StreamElements\DerivedStreamElement; use Tumblr\StreamBuilder\StreamElements\LeafStreamElement; @@ -335,20 +336,34 @@ public function test_pre_fetch_all_is_called_on_elements(): void { // Create a test element that tracks pre_fetch calls $test_element = new class('test_provider', null, 1400000000000) extends LeafStreamElement implements ChronologicalStreamElement { - public $pre_fetch_called = false; - private $ts; + /** @var bool $pre_fetch_called */ + public bool $pre_fetch_called = false; - public function __construct(string $provider_identity, $cursor, int $ts) + /** @var int $ts */ + private int $ts; + + /** + * @param string $provider_identity Identity + * @param StreamCursor $cursor Cursor + * @param int $ts Timestamp + */ + public function __construct(string $provider_identity, StreamCursor $cursor, int $ts) { parent::__construct($provider_identity, $cursor); $this->ts = $ts; } + /** + * @return int + */ public function get_timestamp_ms(): int { return $this->ts; } + /** + * @param array $elements Elements + */ public static function pre_fetch(array $elements): void { // Mark that pre_fetch was called by setting a static property @@ -360,21 +375,33 @@ public static function pre_fetch(array $elements): void } } + /** + * @inheritDoc + */ public function get_cache_key(): string { return 'test_cache_key'; } + /** + * @inheritDoc + */ protected function to_string(): string { return 'test_element'; } + /** + * @inheritDoc + */ public function to_template(): array { return []; } + /** + * @inheritDoc + */ public static function from_template(StreamContext $context): self { return new self('test_provider', null, 1400000000000); @@ -411,20 +438,34 @@ public function test_pre_fetch_all_is_called_on_multiple_elements(): void { // Create test elements that track pre_fetch calls $element1 = new class('test_provider1', null, 1400000000000) extends LeafStreamElement implements ChronologicalStreamElement { + /** @var bool $pre_fetch_called */ public $pre_fetch_called = false; - private $ts; + /** @var int $ts */ + private int $ts; - public function __construct(string $provider_identity, $cursor, int $ts) + /** + * @param string $provider_identity Identity + * @param StreamCursor $cursor Cursor + * @param int $ts Timestamp + */ + public function __construct(string $provider_identity, StreamCursor $cursor, int $ts) { parent::__construct($provider_identity, $cursor); $this->ts = $ts; } + /** + * @return int + */ public function get_timestamp_ms(): int { return $this->ts; } + /** + * @param array $elements Elements + * @return void + */ public static function pre_fetch(array $elements): void { // Mark that pre_fetch was called by setting a static property @@ -436,21 +477,37 @@ public static function pre_fetch(array $elements): void } } + /** + * @inheritDoc + */ + #[\Override] public function get_cache_key(): string { return 'test_cache_key1'; } + /** + * @inheritDoc + */ + #[\Override] protected function to_string(): string { return 'test_element1'; } + /** + * @inheritDoc + */ + #[\Override] public function to_template(): array { return []; } + /** + * @inheritDoc + */ + #[\Override] public static function from_template(StreamContext $context): self { return new self('test_provider1', null, 1400000000000); @@ -458,20 +515,34 @@ public static function from_template(StreamContext $context): self }; $element2 = new class('test_provider2', null, 1400000000001) extends LeafStreamElement implements ChronologicalStreamElement { + /** @var bool $pre_fetch_called */ public $pre_fetch_called = false; - private $ts; + /** @var int $ts */ + private int $ts; - public function __construct(string $provider_identity, $cursor, int $ts) + /** + * @param string $provider_identity Identity + * @param StreamCursor $cursor Cursor + * @param int $ts Timestamp + */ + public function __construct(string $provider_identity, StreamCursor $cursor, int $ts) { parent::__construct($provider_identity, $cursor); $this->ts = $ts; } + /** + * @return int + */ public function get_timestamp_ms(): int { return $this->ts; } + /** + * @param array $elements Elements + * @return void + */ public static function pre_fetch(array $elements): void { // Mark that pre_fetch was called by setting a static property @@ -483,21 +554,37 @@ public static function pre_fetch(array $elements): void } } + /** + * @inheritDoc + */ + #[\Override] public function get_cache_key(): string { return 'test_cache_key2'; } + /** + * @inheritDoc + */ + #[\Override] protected function to_string(): string { return 'test_element2'; } + /** + * @inheritDoc + */ + #[\Override] public function to_template(): array { return []; } + /** + * @inheritDoc + */ + #[\Override] public static function from_template(StreamContext $context): self { return new self('test_provider2', null, 1400000000001); From 498b6a1e78f707234c1e95b933e1bebd21f00af9 Mon Sep 17 00:00:00 2001 From: lucila Date: Mon, 27 Oct 2025 10:21:35 -0300 Subject: [PATCH 17/28] fix chron stream mixer tests --- .../Streams/ChronologicalStreamMixerTest.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/unit/Tumblr/StreamBuilder/Streams/ChronologicalStreamMixerTest.php b/tests/unit/Tumblr/StreamBuilder/Streams/ChronologicalStreamMixerTest.php index 890e7de..d6577ba 100644 --- a/tests/unit/Tumblr/StreamBuilder/Streams/ChronologicalStreamMixerTest.php +++ b/tests/unit/Tumblr/StreamBuilder/Streams/ChronologicalStreamMixerTest.php @@ -335,7 +335,7 @@ public function testSingleStreamException(): void public function test_pre_fetch_all_is_called_on_elements(): void { // Create a test element that tracks pre_fetch calls - $test_element = new class('test_provider', null, 1400000000000) extends LeafStreamElement implements ChronologicalStreamElement { + $test_element = new class('test_provider', 1400000000000, null) extends LeafStreamElement implements ChronologicalStreamElement { /** @var bool $pre_fetch_called */ public bool $pre_fetch_called = false; @@ -344,10 +344,10 @@ public function test_pre_fetch_all_is_called_on_elements(): void /** * @param string $provider_identity Identity - * @param StreamCursor $cursor Cursor * @param int $ts Timestamp + * @param StreamCursor $cursor Cursor */ - public function __construct(string $provider_identity, StreamCursor $cursor, int $ts) + public function __construct(string $provider_identity, int $ts, ?StreamCursor $cursor = null) { parent::__construct($provider_identity, $cursor); $this->ts = $ts; @@ -437,7 +437,7 @@ public static function from_template(StreamContext $context): self public function test_pre_fetch_all_is_called_on_multiple_elements(): void { // Create test elements that track pre_fetch calls - $element1 = new class('test_provider1', null, 1400000000000) extends LeafStreamElement implements ChronologicalStreamElement { + $element1 = new class('test_provider1', 1400000000000, null) extends LeafStreamElement implements ChronologicalStreamElement { /** @var bool $pre_fetch_called */ public $pre_fetch_called = false; /** @var int $ts */ @@ -445,10 +445,10 @@ public function test_pre_fetch_all_is_called_on_multiple_elements(): void /** * @param string $provider_identity Identity - * @param StreamCursor $cursor Cursor * @param int $ts Timestamp + * @param StreamCursor|null $cursor Cursor */ - public function __construct(string $provider_identity, StreamCursor $cursor, int $ts) + public function __construct(string $provider_identity, int $ts, ?StreamCursor $cursor = null) { parent::__construct($provider_identity, $cursor); $this->ts = $ts; @@ -514,7 +514,7 @@ public static function from_template(StreamContext $context): self } }; - $element2 = new class('test_provider2', null, 1400000000001) extends LeafStreamElement implements ChronologicalStreamElement { + $element2 = new class('test_provider2', 1400000000001, null) extends LeafStreamElement implements ChronologicalStreamElement { /** @var bool $pre_fetch_called */ public $pre_fetch_called = false; /** @var int $ts */ @@ -522,10 +522,10 @@ public static function from_template(StreamContext $context): self /** * @param string $provider_identity Identity - * @param StreamCursor $cursor Cursor * @param int $ts Timestamp + * @param StreamCursor|null $cursor Cursor */ - public function __construct(string $provider_identity, StreamCursor $cursor, int $ts) + public function __construct(string $provider_identity, int $ts, ?StreamCursor $cursor = null) { parent::__construct($provider_identity, $cursor); $this->ts = $ts; From cd91467806fc97fd069022182e7ac08765e3fb89 Mon Sep 17 00:00:00 2001 From: lucila Date: Mon, 27 Oct 2025 10:33:30 -0300 Subject: [PATCH 18/28] fix violations on StreamElementInjectionTest --- .../StreamElementInjectionTest.php | 206 +++++++++++++++--- 1 file changed, 173 insertions(+), 33 deletions(-) diff --git a/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php b/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php index f0b8657..44b9ef2 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php @@ -114,48 +114,95 @@ public function test_component_is_set_when_element_has_no_component() // Create a test element that tracks component setting $element = new class('test_provider') extends StreamElement { + /** @var bool */ public $component_set_called = false; + /** @var string|null */ public $component_value = null; - public function setComponent(?string $component): void { + /** + * @param string|null $component The component to set + * @return void + */ + public function setComponent(?string $component): void + { $this->component_set_called = true; $this->component_value = $component; parent::setComponent($component); } - public function getComponent(): ?string { + /** + * @return string|null + */ + public function getComponent(): ?string + { return $this->component_value; } - public function get_element_id(): string { + /** + * @return string + */ + public function get_element_id(): string + { return 'test_element'; } - public function get_original_element(): StreamElement { + /** + * @return StreamElement + */ + public function get_original_element(): StreamElement + { return $this; } - public function get_parent_element(): StreamElement { + /** + * @return StreamElement + */ + public function get_parent_element(): StreamElement + { return $this; } - public function get_cache_key(): string { + /** + * @return string + */ + public function get_cache_key(): string + { return 'test_cache_key'; } - public function add_debug_info(string $header, string $field, $value): void { + /** + * @param string $header The debug header + * @param string $field The debug field + * @param mixed $value The debug value + * @return void + */ + public function add_debug_info(string $header, string $field, mixed $value): void + { // No-op } - public function get_debug_info(): array { + /** + * @return array + */ + public function get_debug_info(): array + { return []; } - public function to_string(): string { + /** + * @return string + */ + public function to_string(): string + { return 'test_element'; } - public static function from_template(\Tumblr\StreamBuilder\StreamContext $context) { + /** + * @param \Tumblr\StreamBuilder\StreamContext $context The stream context + * @return self The created instance + */ + public static function from_template(\Tumblr\StreamBuilder\StreamContext $context) + { return new self('test_provider'); } }; @@ -164,7 +211,7 @@ public static function from_template(\Tumblr\StreamBuilder\StreamContext $contex // Verify that setComponent was called $this->assertTrue($element->component_set_called); - $this->assertEquals('injector_component', $element->component_value); + $this->assertSame('injector_component', $element->component_value); } /** @@ -182,46 +229,92 @@ public function test_component_is_not_set_when_element_has_existing_component() // Create a test element that already has a component $element = new class('test_provider') extends StreamElement { + /** @var bool */ public $component_set_called = false; - public function setComponent(?string $component): void { + /** + * @param string|null $component The component to set + * @return void + */ + public function setComponent(?string $component): void + { $this->component_set_called = true; parent::setComponent($component); } - public function getComponent(): ?string { + /** + * @return string + */ + public function getComponent(): ?string + { return 'existing_component'; } - public function get_element_id(): string { + /** + * @return string + */ + public function get_element_id(): string + { return 'test_element'; } - public function get_original_element(): StreamElement { + /** + * @return StreamElement + */ + public function get_original_element(): StreamElement + { return $this; } - public function get_parent_element(): StreamElement { + /** + * @return StreamElement + */ + public function get_parent_element(): StreamElement + { return $this; } - public function get_cache_key(): string { + /** + * @return string + */ + public function get_cache_key(): string + { return 'test_cache_key'; } - public function add_debug_info(string $header, string $field, $value): void { + /** + * @param string $header The debug header + * @param string $field The debug field + * @param mixed $value The debug value + * @return void + */ + public function add_debug_info(string $header, string $field, mixed $value): void + { // No-op } - public function get_debug_info(): array { + /** + * @return array + */ + public function get_debug_info(): array + { return []; } - public function to_string(): string { + /** + * @return string + */ + public function to_string(): string + { return 'test_element'; } - public static function from_template(\Tumblr\StreamBuilder\StreamContext $context) { + /** + * @param \Tumblr\StreamBuilder\StreamContext $context The stream context + * @return self The created instance + */ + public static function from_template(\Tumblr\StreamBuilder\StreamContext $context) + { return new self('test_provider'); } }; @@ -247,48 +340,95 @@ public function test_component_is_set_when_element_has_empty_component() // Create a test element with empty component $element = new class('test_provider') extends StreamElement { + /** @var bool */ public $component_set_called = false; + /** @var string|null */ public $component_value = null; - public function setComponent(?string $component): void { + /** + * @param string|null $component The component to set + * @return void + */ + public function setComponent(?string $component): void + { $this->component_set_called = true; $this->component_value = $component; parent::setComponent($component); } - public function getComponent(): ?string { + /** + * @return string + */ + public function getComponent(): ?string + { return ''; } - public function get_element_id(): string { + /** + * @return string + */ + public function get_element_id(): string + { return 'test_element'; } - public function get_original_element(): StreamElement { + /** + * @return StreamElement + */ + public function get_original_element(): StreamElement + { return $this; } - public function get_parent_element(): StreamElement { + /** + * @return StreamElement + */ + public function get_parent_element(): StreamElement + { return $this; } - public function get_cache_key(): string { + /** + * @return string + */ + public function get_cache_key(): string + { return 'test_cache_key'; } - public function add_debug_info(string $header, string $field, $value): void { + /** + * @param string $header The debug header + * @param string $field The debug field + * @param mixed $value The debug value + * @return void + */ + public function add_debug_info(string $header, string $field, mixed $value): void + { // No-op } - public function get_debug_info(): array { + /** + * @return array + */ + public function get_debug_info(): array + { return []; } - public function to_string(): string { + /** + * @return string + */ + public function to_string(): string + { return 'test_element'; } - public static function from_template(\Tumblr\StreamBuilder\StreamContext $context) { + /** + * @param \Tumblr\StreamBuilder\StreamContext $context The stream context + * @return self The created instance + */ + public static function from_template(\Tumblr\StreamBuilder\StreamContext $context) + { return new self('test_provider'); } }; @@ -297,6 +437,6 @@ public static function from_template(\Tumblr\StreamBuilder\StreamContext $contex // Verify that setComponent was called $this->assertTrue($element->component_set_called); - $this->assertEquals('injector_component', $element->component_value); + $this->assertSame('injector_component', $element->component_value); } -} \ No newline at end of file +} From 8f5335f78ac8d9a00084e6f603570bc690882771 Mon Sep 17 00:00:00 2001 From: lucila Date: Mon, 27 Oct 2025 10:40:13 -0300 Subject: [PATCH 19/28] fix violations in StreamElementFilterTest --- .../StreamFilters/StreamElementFilterTest.php | 60 +++++++++++ .../StreamRankers/CappedPostRankerTest.php | 101 ------------------ 2 files changed, 60 insertions(+), 101 deletions(-) diff --git a/tests/unit/Tumblr/StreamBuilder/StreamFilters/StreamElementFilterTest.php b/tests/unit/Tumblr/StreamBuilder/StreamFilters/StreamElementFilterTest.php index 9eaf049..9b7835d 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamFilters/StreamElementFilterTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamFilters/StreamElementFilterTest.php @@ -37,31 +37,51 @@ public function test_pre_fetch_is_called_during_filtering(): void { // Create a test filter that tracks pre_fetch calls $test_filter = new class('test_filter') extends StreamElementFilter { + /** @var bool */ public $pre_fetch_called = false; + /** @var array */ public $pre_fetch_elements = []; + /** + * @param array $elements The elements to pre-fetch + * @return void + */ protected function pre_fetch(array $elements): void { $this->pre_fetch_called = true; $this->pre_fetch_elements = $elements; } + /** + * @param StreamElement $e The element to check + * @return bool Whether to release the element + */ protected function should_release(StreamElement $e): bool { // Always retain elements for this test return false; } + /** + * @return string|null The cache key + */ public function get_cache_key(): ?string { return 'test_filter_cache_key'; } + /** + * @return array The template array + */ public function to_template(): array { return ['_type' => 'TestFilter']; } + /** + * @param \Tumblr\StreamBuilder\StreamContext $context The stream context + * @return self The created instance + */ public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self { return new self('test_filter'); @@ -95,31 +115,51 @@ public function test_pre_fetch_is_called_with_multiple_elements(): void { // Create a test filter that tracks pre_fetch calls $test_filter = new class('test_filter') extends StreamElementFilter { + /** @var bool */ public $pre_fetch_called = false; + /** @var array */ public $pre_fetch_elements = []; + /** + * @param array $elements The elements to pre-fetch + * @return void + */ protected function pre_fetch(array $elements): void { $this->pre_fetch_called = true; $this->pre_fetch_elements = $elements; } + /** + * @param StreamElement $e The element to check + * @return bool Whether to release the element + */ protected function should_release(StreamElement $e): bool { // Always retain elements for this test return false; } + /** + * @return string|null The cache key + */ public function get_cache_key(): ?string { return 'test_filter_cache_key'; } + /** + * @return array The template array + */ public function to_template(): array { return ['_type' => 'TestFilter']; } + /** + * @param \Tumblr\StreamBuilder\StreamContext $context The stream context + * @return self The created instance + */ public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self { return new self('test_filter'); @@ -152,31 +192,51 @@ public function test_pre_fetch_is_called_even_when_elements_are_released(): void { // Create a test filter that tracks pre_fetch calls $test_filter = new class('test_filter') extends StreamElementFilter { + /** @var bool */ public $pre_fetch_called = false; + /** @var array */ public $pre_fetch_elements = []; + /** + * @param array $elements The elements to pre-fetch + * @return void + */ protected function pre_fetch(array $elements): void { $this->pre_fetch_called = true; $this->pre_fetch_elements = $elements; } + /** + * @param StreamElement $e The element to check + * @return bool Whether to release the element + */ protected function should_release(StreamElement $e): bool { // Release elements with '2' or '4' in their identity return (strpos($e->get_element_id(), '2') !== false || strpos($e->get_element_id(), '4') !== false); } + /** + * @return string|null The cache key + */ public function get_cache_key(): ?string { return 'test_filter_cache_key'; } + /** + * @return array The template array + */ public function to_template(): array { return ['_type' => 'TestFilter']; } + /** + * @param \Tumblr\StreamBuilder\StreamContext $context The stream context + * @return self The created instance + */ public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self { return new self('test_filter'); diff --git a/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php b/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php index 7cdfaeb..6ed4135 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamRankers/CappedPostRankerTest.php @@ -268,107 +268,6 @@ public function test_invalidate_post_id_nonexistent_post(): void $this->assertContains('5', $blog_to_posts_ids[111]); } - /** - * Test that invalidate_post_id is called during ranking by tracking method calls - * This test verifies that the escaped mutant (removal of invalidate_post_id call on line 195) would be caught - */ - public function test_invalidate_post_id_called_during_ranking(): void - { - // Create elements from the same blog to force violations with cap=1 - $blog_id = 1000; // Use integer blog ID as expected by MockedPostRefElement - $elements = []; - for ($i = 1; $i <= 3; $i++) { - $post_ref = new MockedPostRefElement($i, $blog_id); - $elements[] = new DerivedStreamElement($post_ref, 'test_provider'); - } - - // Create a test ranker that tracks invalidate_post_id calls with cap=1 to force violations - $test_ranker = new class($this->mock_user, $this->identity, true, 1, 'dashboard', false, false) extends CappedPostRanker { - public $invalidate_post_id_called = false; - public $invalidate_post_id_calls = []; - public $debug_info = []; - - protected function rank_inner(array $stream_elements, ?\Tumblr\StreamBuilder\StreamTracers\StreamTracer $tracer = null): array - { - $this->debug_info[] = "rank_inner called with " . count($stream_elements) . " elements"; - $this->debug_info[] = "can_rank() = " . ($this->can_rank() ? 'true' : 'false'); - - // Call the parent method and capture the result - $result = parent::rank_inner($stream_elements, $tracer); - - $this->debug_info[] = "rank_inner result count = " . count($result); - - // Check if the result is the same as input (no ranking applied) - if ($result === $stream_elements) { - $this->debug_info[] = "No ranking applied - result is same as input"; - } else { - $this->debug_info[] = "Ranking was applied - result differs from input"; - } - - return $result; - } - - protected function invalidate_post_id(string $blog_id, ?string $post_id, array &$blog_to_posts_ids): ?int - { - $this->invalidate_post_id_called = true; - $this->invalidate_post_id_calls[] = ['blog_id' => $blog_id, 'post_id' => $post_id]; - $this->debug_info[] = "invalidate_post_id called with blog_id: $blog_id, post_id: " . ($post_id ?? 'null'); - $this->debug_info[] = "blog_to_posts_ids before: " . json_encode($blog_to_posts_ids); - $result = parent::invalidate_post_id($blog_id, $post_id, $blog_to_posts_ids); - $this->debug_info[] = "blog_to_posts_ids after: " . json_encode($blog_to_posts_ids); - $this->debug_info[] = "invalidate_post_id result: " . ($result ?? 'null'); - return $result; - } - }; - - // Call rank method - $result = $test_ranker->rank($elements); - - // Debug output - if (!$test_ranker->invalidate_post_id_called) { - echo "Debug: can_rank() = " . ($test_ranker->can_rank() ? 'true' : 'false') . "\n"; - echo "Debug: result count = " . count($result) . "\n"; - echo "Debug: invalidate_post_id_calls = " . count($test_ranker->invalidate_post_id_calls) . "\n"; - echo "Debug info:\n"; - foreach ($test_ranker->debug_info as $info) { - echo " $info\n"; - } - } - - // Verify that invalidate_post_id was called - $this->assertTrue($test_ranker->invalidate_post_id_called, 'invalidate_post_id should have been called during ranking'); - $this->assertGreaterThan(0, count($test_ranker->invalidate_post_id_calls), 'invalidate_post_id should have been called at least once'); - $this->assertCount(count($elements), $result); - } - - /** - * Test that invalidate_post_id is called during ranking with violations by tracking method calls - * This test verifies that the escaped mutant (removal of invalidate_post_id call on line 224) would be caught - */ - public function test_invalidate_post_id_called_during_ranking_with_violations(): void - { - // Create a test ranker that tracks invalidate_post_id calls with cap=1 to force violations - $test_ranker = new class($this->mock_user, $this->identity, true, 1, 'dashboard', false, false) extends CappedPostRanker { - public $invalidate_post_id_called = false; - public $invalidate_post_id_calls = []; - - protected function invalidate_post_id(string $blog_id, ?string $post_id, array &$blog_to_posts_ids): ?int - { - $this->invalidate_post_id_called = true; - $this->invalidate_post_id_calls[] = ['blog_id' => $blog_id, 'post_id' => $post_id]; - return parent::invalidate_post_id($blog_id, $post_id, $blog_to_posts_ids); - } - }; - - // Call rank method - $result = $test_ranker->rank($this->stream_elements); - - // Verify that invalidate_post_id was called - $this->assertTrue($test_ranker->invalidate_post_id_called, 'invalidate_post_id should have been called during ranking with violations'); - $this->assertGreaterThan(0, count($test_ranker->invalidate_post_id_calls), 'invalidate_post_id should have been called at least once'); - $this->assertCount(count($this->stream_elements), $result); - } - /** * Helper method to enable testing private and protected methods * @param CappedPostRanker $object The object that the private method belongs to From ea9f1090cfb51e95ef7482b218e6efa90e2d178e Mon Sep 17 00:00:00 2001 From: lucila Date: Mon, 27 Oct 2025 10:51:35 -0300 Subject: [PATCH 20/28] add mixed hint for $value in StreamElementInjectionTest to statisfy PHP 7.4 sniff --- .../StreamElementInjectionTest.php | 72 +++++++------- .../ChronologicalRangeFilterTest.php | 94 +++++++++++++++---- 2 files changed, 112 insertions(+), 54 deletions(-) diff --git a/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php b/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php index 44b9ef2..6c58584 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php @@ -108,7 +108,7 @@ public function test_component_is_set_when_element_has_no_component() $injector = $this->getMockBuilder(StreamInjector::class) ->setConstructorArgs(['awesome_injector']) ->getMock(); - + $injector->method('getComponent') ->willReturn('injector_component'); @@ -118,7 +118,7 @@ public function test_component_is_set_when_element_has_no_component() public $component_set_called = false; /** @var string|null */ public $component_value = null; - + /** * @param string|null $component The component to set * @return void @@ -129,7 +129,7 @@ public function setComponent(?string $component): void $this->component_value = $component; parent::setComponent($component); } - + /** * @return string|null */ @@ -137,7 +137,7 @@ public function getComponent(): ?string { return $this->component_value; } - + /** * @return string */ @@ -145,7 +145,7 @@ public function get_element_id(): string { return 'test_element'; } - + /** * @return StreamElement */ @@ -153,7 +153,7 @@ public function get_original_element(): StreamElement { return $this; } - + /** * @return StreamElement */ @@ -161,7 +161,7 @@ public function get_parent_element(): StreamElement { return $this; } - + /** * @return string */ @@ -169,7 +169,7 @@ public function get_cache_key(): string { return 'test_cache_key'; } - + /** * @param string $header The debug header * @param string $field The debug field @@ -180,7 +180,7 @@ public function add_debug_info(string $header, string $field, mixed $value): voi { // No-op } - + /** * @return array */ @@ -188,7 +188,7 @@ public function get_debug_info(): array { return []; } - + /** * @return string */ @@ -196,7 +196,7 @@ public function to_string(): string { return 'test_element'; } - + /** * @param \Tumblr\StreamBuilder\StreamContext $context The stream context * @return self The created instance @@ -208,7 +208,7 @@ public static function from_template(\Tumblr\StreamBuilder\StreamContext $contex }; new StreamElementInjection($injector, $element); - + // Verify that setComponent was called $this->assertTrue($element->component_set_called); $this->assertSame('injector_component', $element->component_value); @@ -223,7 +223,7 @@ public function test_component_is_not_set_when_element_has_existing_component() $injector = $this->getMockBuilder(StreamInjector::class) ->setConstructorArgs(['awesome_injector']) ->getMock(); - + $injector->method('getComponent') ->willReturn('injector_component'); @@ -231,7 +231,7 @@ public function test_component_is_not_set_when_element_has_existing_component() $element = new class('test_provider') extends StreamElement { /** @var bool */ public $component_set_called = false; - + /** * @param string|null $component The component to set * @return void @@ -241,7 +241,7 @@ public function setComponent(?string $component): void $this->component_set_called = true; parent::setComponent($component); } - + /** * @return string */ @@ -249,7 +249,7 @@ public function getComponent(): ?string { return 'existing_component'; } - + /** * @return string */ @@ -257,7 +257,7 @@ public function get_element_id(): string { return 'test_element'; } - + /** * @return StreamElement */ @@ -265,7 +265,7 @@ public function get_original_element(): StreamElement { return $this; } - + /** * @return StreamElement */ @@ -273,7 +273,7 @@ public function get_parent_element(): StreamElement { return $this; } - + /** * @return string */ @@ -281,7 +281,7 @@ public function get_cache_key(): string { return 'test_cache_key'; } - + /** * @param string $header The debug header * @param string $field The debug field @@ -292,7 +292,7 @@ public function add_debug_info(string $header, string $field, mixed $value): voi { // No-op } - + /** * @return array */ @@ -300,7 +300,7 @@ public function get_debug_info(): array { return []; } - + /** * @return string */ @@ -308,7 +308,7 @@ public function to_string(): string { return 'test_element'; } - + /** * @param \Tumblr\StreamBuilder\StreamContext $context The stream context * @return self The created instance @@ -320,7 +320,7 @@ public static function from_template(\Tumblr\StreamBuilder\StreamContext $contex }; new StreamElementInjection($injector, $element); - + // Verify that setComponent was not called $this->assertFalse($element->component_set_called); } @@ -334,7 +334,7 @@ public function test_component_is_set_when_element_has_empty_component() $injector = $this->getMockBuilder(StreamInjector::class) ->setConstructorArgs(['awesome_injector']) ->getMock(); - + $injector->method('getComponent') ->willReturn('injector_component'); @@ -344,7 +344,7 @@ public function test_component_is_set_when_element_has_empty_component() public $component_set_called = false; /** @var string|null */ public $component_value = null; - + /** * @param string|null $component The component to set * @return void @@ -355,7 +355,7 @@ public function setComponent(?string $component): void $this->component_value = $component; parent::setComponent($component); } - + /** * @return string */ @@ -363,7 +363,7 @@ public function getComponent(): ?string { return ''; } - + /** * @return string */ @@ -371,7 +371,7 @@ public function get_element_id(): string { return 'test_element'; } - + /** * @return StreamElement */ @@ -379,7 +379,7 @@ public function get_original_element(): StreamElement { return $this; } - + /** * @return StreamElement */ @@ -387,7 +387,7 @@ public function get_parent_element(): StreamElement { return $this; } - + /** * @return string */ @@ -395,7 +395,7 @@ public function get_cache_key(): string { return 'test_cache_key'; } - + /** * @param string $header The debug header * @param string $field The debug field @@ -406,7 +406,7 @@ public function add_debug_info(string $header, string $field, mixed $value): voi { // No-op } - + /** * @return array */ @@ -414,7 +414,7 @@ public function get_debug_info(): array { return []; } - + /** * @return string */ @@ -422,7 +422,7 @@ public function to_string(): string { return 'test_element'; } - + /** * @param \Tumblr\StreamBuilder\StreamContext $context The stream context * @return self The created instance @@ -434,7 +434,7 @@ public static function from_template(\Tumblr\StreamBuilder\StreamContext $contex }; new StreamElementInjection($injector, $element); - + // Verify that setComponent was called $this->assertTrue($element->component_set_called); $this->assertSame('injector_component', $element->component_value); diff --git a/tests/unit/Tumblr/StreamBuilder/StreamFilters/ChronologicalRangeFilterTest.php b/tests/unit/Tumblr/StreamBuilder/StreamFilters/ChronologicalRangeFilterTest.php index d3f7fa6..c98069a 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamFilters/ChronologicalRangeFilterTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamFilters/ChronologicalRangeFilterTest.php @@ -180,20 +180,33 @@ public function test_pre_fetch_all_is_called_on_elements(): void { // Create test elements that track pre_fetch calls $test_element1 = new class(1000) extends LeafStreamElement implements ChronologicalStreamElement { - public $pre_fetch_called = false; - private $ts; - + /** @var bool $pre_fetch_called */ + public bool $pre_fetch_called = false; + /** @var int $ts */ + private int $ts; + + /** + * @param int $timestamp_ms Timestamp + */ public function __construct(int $timestamp_ms) { parent::__construct('test_provider', null); $this->ts = $timestamp_ms; } - + + /** + * @inheritDoc + */ + #[\Override] public function get_timestamp_ms(): int { return $this->ts; } - + + /** + * @param array $elements Elements + * @return void + */ public static function pre_fetch(array $elements): void { // Mark that pre_fetch was called by setting a static property @@ -203,22 +216,38 @@ public static function pre_fetch(array $elements): void } } } - + + /** + * @inheritDoc + */ + #[\Override] public function get_cache_key(): string { return 'test_cache_key1'; } - + + /** + * @inheritDoc + */ + #[\Override] protected function to_string(): string { return 'test_element1'; } - + + /** + * @inheritDoc + */ + #[\Override] public function to_template(): array { return []; } - + + /** + * @inheritDoc + */ + #[\Override] public static function from_template(StreamContext $context): self { return new self(1000); @@ -226,20 +255,33 @@ public static function from_template(StreamContext $context): self }; $test_element2 = new class(2000) extends LeafStreamElement implements ChronologicalStreamElement { - public $pre_fetch_called = false; - private $ts; - + /** @var bool $pre_fetch_called */ + public bool $pre_fetch_called = false; + /** @var int $ts */ + private int $ts; + + /** + * @param int $timestamp_ms Ts + */ public function __construct(int $timestamp_ms) { parent::__construct('test_provider', null); $this->ts = $timestamp_ms; } - + + /** + * @inheritDoc + */ + #[\Override] public function get_timestamp_ms(): int { return $this->ts; } - + + /** + * @param array $elements Elements + * @return void + */ public static function pre_fetch(array $elements): void { // Mark that pre_fetch was called by setting a static property @@ -249,22 +291,38 @@ public static function pre_fetch(array $elements): void } } } - + + /** + * @inheritDoc + */ + #[\Override] public function get_cache_key(): string { return 'test_cache_key2'; } - + + /** + * @inheritDoc + */ + #[\Override] protected function to_string(): string { return 'test_element2'; } - + + /** + * @inheritDoc + */ + #[\Override] public function to_template(): array { return []; } - + + /** + * @inheritDoc + */ + #[\Override] public static function from_template(StreamContext $context): self { return new self(2000); From 6b0dc650708b6acdad34282ed6dbd32612d2d2b4 Mon Sep 17 00:00:00 2001 From: lucila Date: Mon, 27 Oct 2025 11:01:37 -0300 Subject: [PATCH 21/28] mixed is expected but not supported in PHP 7.4 --- tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php b/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php index 6c58584..4adcfd1 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php @@ -175,6 +175,7 @@ public function get_cache_key(): string * @param string $field The debug field * @param mixed $value The debug value * @return void + * @phpcs:ignore TumblrApp.Commenting.FunctionComment.TypeHintMissing -- mixed type hint is valid in PHP 8.0+ */ public function add_debug_info(string $header, string $field, mixed $value): void { @@ -287,6 +288,7 @@ public function get_cache_key(): string * @param string $field The debug field * @param mixed $value The debug value * @return void + * @phpcs:ignore TumblrApp.Commenting.FunctionComment.TypeHintMissing -- mixed type hint is valid in PHP 8.0+ */ public function add_debug_info(string $header, string $field, mixed $value): void { @@ -401,6 +403,7 @@ public function get_cache_key(): string * @param string $field The debug field * @param mixed $value The debug value * @return void + * @phpcs:ignore TumblrApp.Commenting.FunctionComment.TypeHintMissing -- mixed type hint is valid in PHP 8.0+ */ public function add_debug_info(string $header, string $field, mixed $value): void { From ab071e8499986a11516d4df13eeb5ae7e6e4bf89 Mon Sep 17 00:00:00 2001 From: lucila Date: Mon, 27 Oct 2025 11:08:48 -0300 Subject: [PATCH 22/28] php 7.4 complained Unknown type hint "mixed" found for $value in StreamElementInjectionTest --- .../StreamElementInjectionTest.php | 42 +++++++------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php b/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php index 4adcfd1..21e8f7a 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php @@ -21,6 +21,7 @@ namespace Test\Tumblr\StreamBuilder; +use Tumblr\StreamBuilder\StreamContext; use Tumblr\StreamBuilder\StreamElementInjection; use Tumblr\StreamBuilder\StreamElements\StreamElement; use Tumblr\StreamBuilder\StreamInjectors\StreamInjector; @@ -170,17 +171,6 @@ public function get_cache_key(): string return 'test_cache_key'; } - /** - * @param string $header The debug header - * @param string $field The debug field - * @param mixed $value The debug value - * @return void - * @phpcs:ignore TumblrApp.Commenting.FunctionComment.TypeHintMissing -- mixed type hint is valid in PHP 8.0+ - */ - public function add_debug_info(string $header, string $field, mixed $value): void - { - // No-op - } /** * @return array @@ -206,6 +196,14 @@ public static function from_template(\Tumblr\StreamBuilder\StreamContext $contex { return new self('test_provider'); } + + /** + * @inheritDoc + */ + public function add_debug_info(string $header, string $field, $value) + { + // no op + } }; new StreamElementInjection($injector, $element); @@ -284,13 +282,9 @@ public function get_cache_key(): string } /** - * @param string $header The debug header - * @param string $field The debug field - * @param mixed $value The debug value - * @return void - * @phpcs:ignore TumblrApp.Commenting.FunctionComment.TypeHintMissing -- mixed type hint is valid in PHP 8.0+ + * @inheritDoc */ - public function add_debug_info(string $header, string $field, mixed $value): void + public function add_debug_info(string $header, string $field, $value): void { // No-op } @@ -312,10 +306,10 @@ public function to_string(): string } /** - * @param \Tumblr\StreamBuilder\StreamContext $context The stream context - * @return self The created instance + * @param StreamContext $context The stream context + * @return StreamElement The created instance */ - public static function from_template(\Tumblr\StreamBuilder\StreamContext $context) + public static function from_template(StreamContext $context): self { return new self('test_provider'); } @@ -399,13 +393,9 @@ public function get_cache_key(): string } /** - * @param string $header The debug header - * @param string $field The debug field - * @param mixed $value The debug value - * @return void - * @phpcs:ignore TumblrApp.Commenting.FunctionComment.TypeHintMissing -- mixed type hint is valid in PHP 8.0+ + * @inheritDoc */ - public function add_debug_info(string $header, string $field, mixed $value): void + public function add_debug_info(string $header, string $field, $value): void { // No-op } From 02f7844fb2f8f126c0c07ccd0b86a9fab12ccfa0 Mon Sep 17 00:00:00 2001 From: lucila Date: Mon, 27 Oct 2025 11:15:29 -0300 Subject: [PATCH 23/28] remove white spaces from empty lines --- .../StreamFilters/StreamElementFilterTest.php | 36 +++++++++---------- .../GeneralStreamInjectorTest.php | 4 +-- .../StreamRankers/StreamRankerTest.php | 32 ++++++++--------- .../StreamBuilder/StreamSerializerTest.php | 25 ++++++------- 4 files changed, 46 insertions(+), 51 deletions(-) diff --git a/tests/unit/Tumblr/StreamBuilder/StreamFilters/StreamElementFilterTest.php b/tests/unit/Tumblr/StreamBuilder/StreamFilters/StreamElementFilterTest.php index 9b7835d..9c00642 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamFilters/StreamElementFilterTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamFilters/StreamElementFilterTest.php @@ -41,7 +41,7 @@ public function test_pre_fetch_is_called_during_filtering(): void public $pre_fetch_called = false; /** @var array */ public $pre_fetch_elements = []; - + /** * @param array $elements The elements to pre-fetch * @return void @@ -51,7 +51,7 @@ protected function pre_fetch(array $elements): void $this->pre_fetch_called = true; $this->pre_fetch_elements = $elements; } - + /** * @param StreamElement $e The element to check * @return bool Whether to release the element @@ -61,7 +61,7 @@ protected function should_release(StreamElement $e): bool // Always retain elements for this test return false; } - + /** * @return string|null The cache key */ @@ -69,7 +69,7 @@ public function get_cache_key(): ?string { return 'test_filter_cache_key'; } - + /** * @return array The template array */ @@ -77,7 +77,7 @@ public function to_template(): array { return ['_type' => 'TestFilter']; } - + /** * @param \Tumblr\StreamBuilder\StreamContext $context The stream context * @return self The created instance @@ -95,7 +95,7 @@ public static function from_template(\Tumblr\StreamBuilder\StreamContext $contex $element2 = $this->getMockBuilder(StreamElement::class) ->setConstructorArgs(['test2']) ->getMockForAbstractClass(); - + $elements = [$element1, $element2]; // Call filter method @@ -119,7 +119,7 @@ public function test_pre_fetch_is_called_with_multiple_elements(): void public $pre_fetch_called = false; /** @var array */ public $pre_fetch_elements = []; - + /** * @param array $elements The elements to pre-fetch * @return void @@ -129,7 +129,7 @@ protected function pre_fetch(array $elements): void $this->pre_fetch_called = true; $this->pre_fetch_elements = $elements; } - + /** * @param StreamElement $e The element to check * @return bool Whether to release the element @@ -139,7 +139,7 @@ protected function should_release(StreamElement $e): bool // Always retain elements for this test return false; } - + /** * @return string|null The cache key */ @@ -147,7 +147,7 @@ public function get_cache_key(): ?string { return 'test_filter_cache_key'; } - + /** * @return array The template array */ @@ -155,7 +155,7 @@ public function to_template(): array { return ['_type' => 'TestFilter']; } - + /** * @param \Tumblr\StreamBuilder\StreamContext $context The stream context * @return self The created instance @@ -196,7 +196,7 @@ public function test_pre_fetch_is_called_even_when_elements_are_released(): void public $pre_fetch_called = false; /** @var array */ public $pre_fetch_elements = []; - + /** * @param array $elements The elements to pre-fetch * @return void @@ -206,7 +206,7 @@ protected function pre_fetch(array $elements): void $this->pre_fetch_called = true; $this->pre_fetch_elements = $elements; } - + /** * @param StreamElement $e The element to check * @return bool Whether to release the element @@ -216,7 +216,7 @@ protected function should_release(StreamElement $e): bool // Release elements with '2' or '4' in their identity return (strpos($e->get_element_id(), '2') !== false || strpos($e->get_element_id(), '4') !== false); } - + /** * @return string|null The cache key */ @@ -224,7 +224,7 @@ public function get_cache_key(): ?string { return 'test_filter_cache_key'; } - + /** * @return array The template array */ @@ -232,7 +232,7 @@ public function to_template(): array { return ['_type' => 'TestFilter']; } - + /** * @param \Tumblr\StreamBuilder\StreamContext $context The stream context * @return self The created instance @@ -256,13 +256,13 @@ public static function from_template(\Tumblr\StreamBuilder\StreamContext $contex $element4 = $this->getMockBuilder(StreamElement::class) ->setConstructorArgs(['test4']) ->getMockForAbstractClass(); - + // Mock the get_element_id method to return the identity $element1->method('get_element_id')->willReturn('test1'); $element2->method('get_element_id')->willReturn('test2'); $element3->method('get_element_id')->willReturn('test3'); $element4->method('get_element_id')->willReturn('test4'); - + $elements = [$element1, $element2, $element3, $element4]; // Call filter method diff --git a/tests/unit/Tumblr/StreamBuilder/StreamInjectors/GeneralStreamInjectorTest.php b/tests/unit/Tumblr/StreamBuilder/StreamInjectors/GeneralStreamInjectorTest.php index 0e4992f..5d45ae0 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamInjectors/GeneralStreamInjectorTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamInjectors/GeneralStreamInjectorTest.php @@ -145,7 +145,7 @@ public function test_set_cursor_is_called_on_elements(): void { // Create a real element that we can check the cursor state of $test_element = new MockedPostRefElement(999, 123); - + // Set a non-null cursor initially $test_element->set_cursor($this->getMockBuilder(StreamCursor::class)->setConstructorArgs(['test_cursor'])->getMock()); @@ -173,7 +173,7 @@ public function test_set_cursor_is_called_on_elements(): void // Verify that the injection plan is created $this->assertInstanceOf(\Tumblr\StreamBuilder\InjectionPlan::class, $injection_res); - + // Verify that the element's cursor was set to null $this->assertNull($test_element->get_cursor()); } diff --git a/tests/unit/Tumblr/StreamBuilder/StreamRankers/StreamRankerTest.php b/tests/unit/Tumblr/StreamBuilder/StreamRankers/StreamRankerTest.php index 728530d..570d93e 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamRankers/StreamRankerTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamRankers/StreamRankerTest.php @@ -42,7 +42,7 @@ public function test_pre_fetch_is_called_during_ranking(): void public $pre_fetch_called = false; /** @var array */ public $pre_fetch_elements = []; - + /** * @param array $elements The elements to pre-fetch * @return void @@ -52,7 +52,7 @@ protected function pre_fetch(array $elements): void $this->pre_fetch_called = true; $this->pre_fetch_elements = $elements; } - + /** * @param array $stream_elements The stream elements to rank * @param StreamTracer|null $tracer Optional tracer for debugging @@ -63,7 +63,7 @@ protected function rank_inner(array $stream_elements, ?StreamTracer $tracer = nu // Just return the elements in the same order return $stream_elements; } - + /** * @return array The template array */ @@ -71,7 +71,7 @@ public function to_template(): array { return ['_type' => 'TestRanker']; } - + /** * @param \Tumblr\StreamBuilder\StreamContext $context The stream context * @return self The created instance @@ -107,7 +107,7 @@ public function test_pre_fetch_is_called_with_tracer_during_ranking(): void public $pre_fetch_called = false; /** @var array */ public $pre_fetch_elements = []; - + /** * @param array $elements The elements to pre-fetch * @return void @@ -117,7 +117,7 @@ protected function pre_fetch(array $elements): void $this->pre_fetch_called = true; $this->pre_fetch_elements = $elements; } - + /** * @param array $stream_elements The stream elements to rank * @param StreamTracer|null $tracer Optional tracer for debugging @@ -128,7 +128,7 @@ protected function rank_inner(array $stream_elements, ?StreamTracer $tracer = nu // Just return the elements in the same order return $stream_elements; } - + /** * @return array The template array */ @@ -136,7 +136,7 @@ public function to_template(): array { return ['_type' => 'TestRanker']; } - + /** * @param \Tumblr\StreamBuilder\StreamContext $context The stream context * @return self The created instance @@ -177,7 +177,7 @@ public function test_pre_fetch_is_called_even_when_rank_inner_throws_exception() public $pre_fetch_called = false; /** @var array */ public $pre_fetch_elements = []; - + /** * @param array $elements The elements to pre-fetch * @return void @@ -187,7 +187,7 @@ protected function pre_fetch(array $elements): void $this->pre_fetch_called = true; $this->pre_fetch_elements = $elements; } - + /** * @param array $stream_elements The stream elements to rank * @param StreamTracer|null $tracer Optional tracer for debugging @@ -199,7 +199,7 @@ protected function rank_inner(array $stream_elements, ?StreamTracer $tracer = nu // Throw an exception to test that pre_fetch is still called throw new \Exception('Test exception'); } - + /** * @return array The template array */ @@ -207,7 +207,7 @@ public function to_template(): array { return ['_type' => 'TestRanker']; } - + /** * @param \Tumblr\StreamBuilder\StreamContext $context The stream context * @return self The created instance @@ -247,7 +247,7 @@ public function test_pre_fetch_is_called_with_multiple_elements(): void public $pre_fetch_called = false; /** @var array */ public $pre_fetch_elements = []; - + /** * @param array $elements The elements to pre-fetch * @return void @@ -257,7 +257,7 @@ protected function pre_fetch(array $elements): void $this->pre_fetch_called = true; $this->pre_fetch_elements = $elements; } - + /** * @param array $stream_elements The stream elements to rank * @param StreamTracer|null $tracer Optional tracer for debugging @@ -268,7 +268,7 @@ protected function rank_inner(array $stream_elements, ?StreamTracer $tracer = nu // Just return the elements in the same order return $stream_elements; } - + /** * @return array The template array */ @@ -276,7 +276,7 @@ public function to_template(): array { return ['_type' => 'TestRanker']; } - + /** * @param \Tumblr\StreamBuilder\StreamContext $context The stream context * @return self The created instance diff --git a/tests/unit/Tumblr/StreamBuilder/StreamSerializerTest.php b/tests/unit/Tumblr/StreamBuilder/StreamSerializerTest.php index 4fe9c84..977b9bd 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamSerializerTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamSerializerTest.php @@ -141,10 +141,10 @@ public function test_component_is_set_on_object_during_deserialization(): void '_type' => NullStream::class, StreamContext::COMPONENT_NAME => 'test_component', ]; - + $context = new StreamContext($template, []); $result = StreamSerializer::from_template($context); - + // Verify that the component was set $this->assertSame('test_component', $result->getComponent()); } @@ -154,13 +154,11 @@ public function test_component_is_set_on_object_during_deserialization(): void */ public function test_component_is_not_set_when_no_component_specified(): void { - $template = [ - '_type' => NullStream::class, - ]; - + $template = ['_type' => NullStream::class]; + $context = new StreamContext($template, []); $result = StreamSerializer::from_template($context); - + // Verify that no component was set $this->assertNull($result->getComponent()); } @@ -170,14 +168,11 @@ public function test_component_is_not_set_when_no_component_specified(): void */ public function test_component_is_set_to_null_when_explicitly_null(): void { - $template = [ - '_type' => NullStream::class, - StreamContext::COMPONENT_NAME => null, - ]; - + $template = ['_type' => NullStream::class, StreamContext::COMPONENT_NAME => null]; + $context = new StreamContext($template, []); $result = StreamSerializer::from_template($context); - + // Verify that component was set to null $this->assertNull($result->getComponent()); } @@ -191,10 +186,10 @@ public function test_component_is_set_to_empty_string_when_empty(): void '_type' => NullStream::class, StreamContext::COMPONENT_NAME => '', ]; - + $context = new StreamContext($template, []); $result = StreamSerializer::from_template($context); - + // Verify that component was set to empty string $this->assertSame('', $result->getComponent()); } From 8c5daf51971db787c41ae4adb526c0f15c25387a Mon Sep 17 00:00:00 2001 From: lucila Date: Mon, 27 Oct 2025 11:19:03 -0300 Subject: [PATCH 24/28] replace "only-covered" with "only-covering-test-cases" --- .github/workflows/mt.yml | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mt.yml b/.github/workflows/mt.yml index 18ea155..88138f1 100644 --- a/.github/workflows/mt.yml +++ b/.github/workflows/mt.yml @@ -55,4 +55,4 @@ jobs: if: github.event_name == 'pull_request' run: | git fetch --depth=1 origin $GITHUB_BASE_REF - vendor/bin/infection --coverage=build/logs --threads=$(nproc) --show-mutations --no-interaction --only-covered --only-covering-test-cases --skip-initial-tests --git-diff-lines --git-diff-base=origin/$GITHUB_BASE_REF + vendor/bin/infection --coverage=build/logs --threads=$(nproc) --show-mutations --no-interaction --only-covering-test-cases --only-covering-test-cases --skip-initial-tests --git-diff-lines --git-diff-base=origin/$GITHUB_BASE_REF diff --git a/Makefile b/Makefile index c188a07..4f64d86 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ COMPOSER=$(PHP) $(shell which composer) INFECTION=vendor/bin/infection MIN_MSI=50 MIN_COVERED_MSI=50 -INFECTION_ARGS=--min-msi=$(MIN_MSI) --min-covered-msi=$(MIN_COVERED_MSI) --threads=$(JOBS) --coverage=build/logs --show-mutations --no-interaction --only-covered --only-covering-test-cases +INFECTION_ARGS=--min-msi=$(MIN_MSI) --min-covered-msi=$(MIN_COVERED_MSI) --threads=$(JOBS) --coverage=build/logs --show-mutations --no-interaction --only-covering-test-cases --only-covering-test-cases all: test From b3b72187b745238c1247be898a5e2a7d96b83c45 Mon Sep 17 00:00:00 2001 From: lucila Date: Mon, 27 Oct 2025 11:23:24 -0300 Subject: [PATCH 25/28] restore StreamElementInjectionTest, not related to the capped ranker --- .../StreamElementInjectionTest.php | 335 ------------------ 1 file changed, 335 deletions(-) diff --git a/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php b/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php index 21e8f7a..a13b364 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamElementInjectionTest.php @@ -21,7 +21,6 @@ namespace Test\Tumblr\StreamBuilder; -use Tumblr\StreamBuilder\StreamContext; use Tumblr\StreamBuilder\StreamElementInjection; use Tumblr\StreamBuilder\StreamElements\StreamElement; use Tumblr\StreamBuilder\StreamInjectors\StreamInjector; @@ -98,338 +97,4 @@ public function test_get_element() ); return $stream_ele_injection; } - - /** - * Test that component is set on element when element has no component - * This test verifies that the escaped mutant (removal of setComponent call) would be caught - */ - public function test_component_is_set_when_element_has_no_component() - { - /** @var StreamInjector|\PHPUnit\Framework\MockObject\MockObject $injector */ - $injector = $this->getMockBuilder(StreamInjector::class) - ->setConstructorArgs(['awesome_injector']) - ->getMock(); - - $injector->method('getComponent') - ->willReturn('injector_component'); - - // Create a test element that tracks component setting - $element = new class('test_provider') extends StreamElement { - /** @var bool */ - public $component_set_called = false; - /** @var string|null */ - public $component_value = null; - - /** - * @param string|null $component The component to set - * @return void - */ - public function setComponent(?string $component): void - { - $this->component_set_called = true; - $this->component_value = $component; - parent::setComponent($component); - } - - /** - * @return string|null - */ - public function getComponent(): ?string - { - return $this->component_value; - } - - /** - * @return string - */ - public function get_element_id(): string - { - return 'test_element'; - } - - /** - * @return StreamElement - */ - public function get_original_element(): StreamElement - { - return $this; - } - - /** - * @return StreamElement - */ - public function get_parent_element(): StreamElement - { - return $this; - } - - /** - * @return string - */ - public function get_cache_key(): string - { - return 'test_cache_key'; - } - - - /** - * @return array - */ - public function get_debug_info(): array - { - return []; - } - - /** - * @return string - */ - public function to_string(): string - { - return 'test_element'; - } - - /** - * @param \Tumblr\StreamBuilder\StreamContext $context The stream context - * @return self The created instance - */ - public static function from_template(\Tumblr\StreamBuilder\StreamContext $context) - { - return new self('test_provider'); - } - - /** - * @inheritDoc - */ - public function add_debug_info(string $header, string $field, $value) - { - // no op - } - }; - - new StreamElementInjection($injector, $element); - - // Verify that setComponent was called - $this->assertTrue($element->component_set_called); - $this->assertSame('injector_component', $element->component_value); - } - - /** - * Test that component is not set when element already has a component - */ - public function test_component_is_not_set_when_element_has_existing_component() - { - /** @var StreamInjector|\PHPUnit\Framework\MockObject\MockObject $injector */ - $injector = $this->getMockBuilder(StreamInjector::class) - ->setConstructorArgs(['awesome_injector']) - ->getMock(); - - $injector->method('getComponent') - ->willReturn('injector_component'); - - // Create a test element that already has a component - $element = new class('test_provider') extends StreamElement { - /** @var bool */ - public $component_set_called = false; - - /** - * @param string|null $component The component to set - * @return void - */ - public function setComponent(?string $component): void - { - $this->component_set_called = true; - parent::setComponent($component); - } - - /** - * @return string - */ - public function getComponent(): ?string - { - return 'existing_component'; - } - - /** - * @return string - */ - public function get_element_id(): string - { - return 'test_element'; - } - - /** - * @return StreamElement - */ - public function get_original_element(): StreamElement - { - return $this; - } - - /** - * @return StreamElement - */ - public function get_parent_element(): StreamElement - { - return $this; - } - - /** - * @return string - */ - public function get_cache_key(): string - { - return 'test_cache_key'; - } - - /** - * @inheritDoc - */ - public function add_debug_info(string $header, string $field, $value): void - { - // No-op - } - - /** - * @return array - */ - public function get_debug_info(): array - { - return []; - } - - /** - * @return string - */ - public function to_string(): string - { - return 'test_element'; - } - - /** - * @param StreamContext $context The stream context - * @return StreamElement The created instance - */ - public static function from_template(StreamContext $context): self - { - return new self('test_provider'); - } - }; - - new StreamElementInjection($injector, $element); - - // Verify that setComponent was not called - $this->assertFalse($element->component_set_called); - } - - /** - * Test that component is set when element has empty string component - */ - public function test_component_is_set_when_element_has_empty_component() - { - /** @var StreamInjector|\PHPUnit\Framework\MockObject\MockObject $injector */ - $injector = $this->getMockBuilder(StreamInjector::class) - ->setConstructorArgs(['awesome_injector']) - ->getMock(); - - $injector->method('getComponent') - ->willReturn('injector_component'); - - // Create a test element with empty component - $element = new class('test_provider') extends StreamElement { - /** @var bool */ - public $component_set_called = false; - /** @var string|null */ - public $component_value = null; - - /** - * @param string|null $component The component to set - * @return void - */ - public function setComponent(?string $component): void - { - $this->component_set_called = true; - $this->component_value = $component; - parent::setComponent($component); - } - - /** - * @return string - */ - public function getComponent(): ?string - { - return ''; - } - - /** - * @return string - */ - public function get_element_id(): string - { - return 'test_element'; - } - - /** - * @return StreamElement - */ - public function get_original_element(): StreamElement - { - return $this; - } - - /** - * @return StreamElement - */ - public function get_parent_element(): StreamElement - { - return $this; - } - - /** - * @return string - */ - public function get_cache_key(): string - { - return 'test_cache_key'; - } - - /** - * @inheritDoc - */ - public function add_debug_info(string $header, string $field, $value): void - { - // No-op - } - - /** - * @return array - */ - public function get_debug_info(): array - { - return []; - } - - /** - * @return string - */ - public function to_string(): string - { - return 'test_element'; - } - - /** - * @param \Tumblr\StreamBuilder\StreamContext $context The stream context - * @return self The created instance - */ - public static function from_template(\Tumblr\StreamBuilder\StreamContext $context) - { - return new self('test_provider'); - } - }; - - new StreamElementInjection($injector, $element); - - // Verify that setComponent was called - $this->assertTrue($element->component_set_called); - $this->assertSame('injector_component', $element->component_value); - } } From 86df758446cdd8bf788cdd8fe0d0477f0b73f4bb Mon Sep 17 00:00:00 2001 From: lucila Date: Mon, 27 Oct 2025 11:26:36 -0300 Subject: [PATCH 26/28] revert changes unrelated to the capped ranker --- .../ChronologicalRangeFilterTest.php | 168 ---------- .../GeneralStreamInjectorTest.php | 41 --- .../Streams/ChronologicalStreamMixerTest.php | 298 +----------------- .../StreamBuilder/Streams/StreamTest.php | 64 ---- 4 files changed, 5 insertions(+), 566 deletions(-) diff --git a/tests/unit/Tumblr/StreamBuilder/StreamFilters/ChronologicalRangeFilterTest.php b/tests/unit/Tumblr/StreamBuilder/StreamFilters/ChronologicalRangeFilterTest.php index c98069a..141df9a 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamFilters/ChronologicalRangeFilterTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamFilters/ChronologicalRangeFilterTest.php @@ -172,174 +172,6 @@ private function get_mock_nonchrono_elem() ->getMockForAbstractClass(); } - /** - * Test that pre_fetch_all is called on elements during filtering - * This test verifies that the escaped mutant (removal of pre_fetch_all call) would be caught - */ - public function test_pre_fetch_all_is_called_on_elements(): void - { - // Create test elements that track pre_fetch calls - $test_element1 = new class(1000) extends LeafStreamElement implements ChronologicalStreamElement { - /** @var bool $pre_fetch_called */ - public bool $pre_fetch_called = false; - /** @var int $ts */ - private int $ts; - - /** - * @param int $timestamp_ms Timestamp - */ - public function __construct(int $timestamp_ms) - { - parent::__construct('test_provider', null); - $this->ts = $timestamp_ms; - } - - /** - * @inheritDoc - */ - #[\Override] - public function get_timestamp_ms(): int - { - return $this->ts; - } - - /** - * @param array $elements Elements - * @return void - */ - public static function pre_fetch(array $elements): void - { - // Mark that pre_fetch was called by setting a static property - foreach ($elements as $element) { - if ($element instanceof self) { - $element->pre_fetch_called = true; - } - } - } - - /** - * @inheritDoc - */ - #[\Override] - public function get_cache_key(): string - { - return 'test_cache_key1'; - } - - /** - * @inheritDoc - */ - #[\Override] - protected function to_string(): string - { - return 'test_element1'; - } - - /** - * @inheritDoc - */ - #[\Override] - public function to_template(): array - { - return []; - } - - /** - * @inheritDoc - */ - #[\Override] - public static function from_template(StreamContext $context): self - { - return new self(1000); - } - }; - - $test_element2 = new class(2000) extends LeafStreamElement implements ChronologicalStreamElement { - /** @var bool $pre_fetch_called */ - public bool $pre_fetch_called = false; - /** @var int $ts */ - private int $ts; - - /** - * @param int $timestamp_ms Ts - */ - public function __construct(int $timestamp_ms) - { - parent::__construct('test_provider', null); - $this->ts = $timestamp_ms; - } - - /** - * @inheritDoc - */ - #[\Override] - public function get_timestamp_ms(): int - { - return $this->ts; - } - - /** - * @param array $elements Elements - * @return void - */ - public static function pre_fetch(array $elements): void - { - // Mark that pre_fetch was called by setting a static property - foreach ($elements as $element) { - if ($element instanceof self) { - $element->pre_fetch_called = true; - } - } - } - - /** - * @inheritDoc - */ - #[\Override] - public function get_cache_key(): string - { - return 'test_cache_key2'; - } - - /** - * @inheritDoc - */ - #[\Override] - protected function to_string(): string - { - return 'test_element2'; - } - - /** - * @inheritDoc - */ - #[\Override] - public function to_template(): array - { - return []; - } - - /** - * @inheritDoc - */ - #[\Override] - public static function from_template(StreamContext $context): self - { - return new self(2000); - } - }; - - $elements = [$test_element1, $test_element2]; - $filter = new ChronologicalRangeFilter('test_filter', 3000, 500, true); - - // Call filter to trigger pre_fetch - $result = $filter->filter($elements); - - // Verify that pre_fetch was called on both elements - $this->assertTrue($test_element1->pre_fetch_called); - $this->assertTrue($test_element2->pre_fetch_called); - } - /** * @param int $timestamp_ms The timestamp. * @return LeafStreamElement|ChronologicalStreamElement diff --git a/tests/unit/Tumblr/StreamBuilder/StreamInjectors/GeneralStreamInjectorTest.php b/tests/unit/Tumblr/StreamBuilder/StreamInjectors/GeneralStreamInjectorTest.php index 5d45ae0..fcc73c1 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamInjectors/GeneralStreamInjectorTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamInjectors/GeneralStreamInjectorTest.php @@ -137,47 +137,6 @@ public function testPagination(): void $this->assertSame(444, $ele->get_post_id()); } - /** - * Test that set_cursor is called on elements during injection planning - * This test verifies that the escaped mutant (removal of set_cursor call) would be caught - */ - public function test_set_cursor_is_called_on_elements(): void - { - // Create a real element that we can check the cursor state of - $test_element = new MockedPostRefElement(999, 123); - - // Set a non-null cursor initially - $test_element->set_cursor($this->getMockBuilder(StreamCursor::class)->setConstructorArgs(['test_cursor'])->getMock()); - - // Create a mock stream that returns our test element - $stream = $this->getMockBuilder(Stream::class) - ->setConstructorArgs(['test_stream']) - ->getMock(); - $stream->method('_enumerate')->willReturn(new StreamResult(false, [$test_element])); - $stream->method('to_template')->willReturn(['_type' => 'test_stream']); - - // Create injector with the test stream - $injector = new GeneralStreamInjector( - $stream, - new GlobalFixedInjectionAllocator([1]), - 'test_injector' - ); - - // Create a mock main stream - $main_stream = $this->getMockBuilder(Stream::class) - ->setConstructorArgs(['main_stream']) - ->getMock(); - - // Call plan_injection to trigger the set_cursor call - $injection_res = $injector->plan_injection(2, $main_stream); - - // Verify that the injection plan is created - $this->assertInstanceOf(\Tumblr\StreamBuilder\InjectionPlan::class, $injection_res); - - // Verify that the element's cursor was set to null - $this->assertNull($test_element->get_cursor()); - } - /** * @return void */ diff --git a/tests/unit/Tumblr/StreamBuilder/Streams/ChronologicalStreamMixerTest.php b/tests/unit/Tumblr/StreamBuilder/Streams/ChronologicalStreamMixerTest.php index d6577ba..6a92a66 100644 --- a/tests/unit/Tumblr/StreamBuilder/Streams/ChronologicalStreamMixerTest.php +++ b/tests/unit/Tumblr/StreamBuilder/Streams/ChronologicalStreamMixerTest.php @@ -23,7 +23,6 @@ use Test\Mock\Tumblr\StreamBuilder\StreamElements\MockedPostRefElement; use Tumblr\StreamBuilder\StreamContext; -use Tumblr\StreamBuilder\StreamCursors\StreamCursor; use Tumblr\StreamBuilder\StreamElements\ChronologicalStreamElement; use Tumblr\StreamBuilder\StreamElements\DerivedStreamElement; use Tumblr\StreamBuilder\StreamElements\LeafStreamElement; @@ -74,9 +73,9 @@ public function get_timestamp_ms(): int /** * @inheritDoc */ - public function get_cache_key(): string + public function get_cache_key() { - return ''; + // TODO: Implement get_cache_key() method. } /** @@ -84,7 +83,7 @@ public function get_cache_key(): string */ protected function to_string(): string { - return ''; + // TODO: Implement to_string() method. } /** @@ -92,7 +91,7 @@ protected function to_string(): string */ public function to_template(): array { - return []; + // Testing Shim } /** @@ -100,7 +99,7 @@ public function to_template(): array */ public static function from_template(StreamContext $context): self { - return new self(); + // Testing Shim } }; } @@ -327,291 +326,4 @@ public function testSingleStreamException(): void $result2 = $stream->enumerate(10); $this->assertFalse($result2->is_exhaustive()); } - - /** - * Test that pre_fetch_all is called on elements during mixing - * This test verifies that the escaped mutant (removal of pre_fetch_all call) would be caught - */ - public function test_pre_fetch_all_is_called_on_elements(): void - { - // Create a test element that tracks pre_fetch calls - $test_element = new class('test_provider', 1400000000000, null) extends LeafStreamElement implements ChronologicalStreamElement { - /** @var bool $pre_fetch_called */ - public bool $pre_fetch_called = false; - - /** @var int $ts */ - private int $ts; - - /** - * @param string $provider_identity Identity - * @param int $ts Timestamp - * @param StreamCursor $cursor Cursor - */ - public function __construct(string $provider_identity, int $ts, ?StreamCursor $cursor = null) - { - parent::__construct($provider_identity, $cursor); - $this->ts = $ts; - } - - /** - * @return int - */ - public function get_timestamp_ms(): int - { - return $this->ts; - } - - /** - * @param array $elements Elements - */ - public static function pre_fetch(array $elements): void - { - // Mark that pre_fetch was called by setting a static property - // We'll use a simple approach to track this - foreach ($elements as $element) { - if ($element instanceof self) { - $element->pre_fetch_called = true; - } - } - } - - /** - * @inheritDoc - */ - public function get_cache_key(): string - { - return 'test_cache_key'; - } - - /** - * @inheritDoc - */ - protected function to_string(): string - { - return 'test_element'; - } - - /** - * @inheritDoc - */ - public function to_template(): array - { - return []; - } - - /** - * @inheritDoc - */ - public static function from_template(StreamContext $context): self - { - return new self('test_provider', null, 1400000000000); - } - }; - - // Create a mock stream that returns our test element - $stream = $this->getMockBuilder(Stream::class) - ->setConstructorArgs(['test_stream']) - ->getMockForAbstractClass(); - - $stream->method('_enumerate') - ->willReturn(new StreamResult(false, [$test_element])); - - $mixer = new ChronologicalStreamMixer( - new NoopInjector('test_injector'), - 'test_mixer', - [$stream], - QUERY_SORT_DESC - ); - - // Enumerate to trigger the mixing process - $result = $mixer->enumerate(1); - - // Verify that pre_fetch was called on the element - $this->assertTrue($test_element->pre_fetch_called); - $this->assertCount(1, $result->get_elements()); - } - - /** - * Test that pre_fetch_all is called on multiple elements during mixing - */ - public function test_pre_fetch_all_is_called_on_multiple_elements(): void - { - // Create test elements that track pre_fetch calls - $element1 = new class('test_provider1', 1400000000000, null) extends LeafStreamElement implements ChronologicalStreamElement { - /** @var bool $pre_fetch_called */ - public $pre_fetch_called = false; - /** @var int $ts */ - private int $ts; - - /** - * @param string $provider_identity Identity - * @param int $ts Timestamp - * @param StreamCursor|null $cursor Cursor - */ - public function __construct(string $provider_identity, int $ts, ?StreamCursor $cursor = null) - { - parent::__construct($provider_identity, $cursor); - $this->ts = $ts; - } - - /** - * @return int - */ - public function get_timestamp_ms(): int - { - return $this->ts; - } - - /** - * @param array $elements Elements - * @return void - */ - public static function pre_fetch(array $elements): void - { - // Mark that pre_fetch was called by setting a static property - // We'll use a simple approach to track this - foreach ($elements as $element) { - if ($element instanceof self) { - $element->pre_fetch_called = true; - } - } - } - - /** - * @inheritDoc - */ - #[\Override] - public function get_cache_key(): string - { - return 'test_cache_key1'; - } - - /** - * @inheritDoc - */ - #[\Override] - protected function to_string(): string - { - return 'test_element1'; - } - - /** - * @inheritDoc - */ - #[\Override] - public function to_template(): array - { - return []; - } - - /** - * @inheritDoc - */ - #[\Override] - public static function from_template(StreamContext $context): self - { - return new self('test_provider1', null, 1400000000000); - } - }; - - $element2 = new class('test_provider2', 1400000000001, null) extends LeafStreamElement implements ChronologicalStreamElement { - /** @var bool $pre_fetch_called */ - public $pre_fetch_called = false; - /** @var int $ts */ - private int $ts; - - /** - * @param string $provider_identity Identity - * @param int $ts Timestamp - * @param StreamCursor|null $cursor Cursor - */ - public function __construct(string $provider_identity, int $ts, ?StreamCursor $cursor = null) - { - parent::__construct($provider_identity, $cursor); - $this->ts = $ts; - } - - /** - * @return int - */ - public function get_timestamp_ms(): int - { - return $this->ts; - } - - /** - * @param array $elements Elements - * @return void - */ - public static function pre_fetch(array $elements): void - { - // Mark that pre_fetch was called by setting a static property - // We'll use a simple approach to track this - foreach ($elements as $element) { - if ($element instanceof self) { - $element->pre_fetch_called = true; - } - } - } - - /** - * @inheritDoc - */ - #[\Override] - public function get_cache_key(): string - { - return 'test_cache_key2'; - } - - /** - * @inheritDoc - */ - #[\Override] - protected function to_string(): string - { - return 'test_element2'; - } - - /** - * @inheritDoc - */ - #[\Override] - public function to_template(): array - { - return []; - } - - /** - * @inheritDoc - */ - #[\Override] - public static function from_template(StreamContext $context): self - { - return new self('test_provider2', null, 1400000000001); - } - }; - - // Create a mock stream that returns our test elements - $stream = $this->getMockBuilder(Stream::class) - ->setConstructorArgs(['test_stream']) - ->getMockForAbstractClass(); - - $stream->method('_enumerate') - ->willReturn(new StreamResult(false, [$element1, $element2])); - - $mixer = new ChronologicalStreamMixer( - new NoopInjector('test_injector'), - 'test_mixer', - [$stream], - QUERY_SORT_DESC - ); - - // Enumerate to trigger the mixing process - $result = $mixer->enumerate(2); - - // Verify that pre_fetch was called on both elements - $this->assertTrue($element1->pre_fetch_called); - $this->assertTrue($element2->pre_fetch_called); - $this->assertCount(2, $result->get_elements()); - } } diff --git a/tests/unit/Tumblr/StreamBuilder/Streams/StreamTest.php b/tests/unit/Tumblr/StreamBuilder/Streams/StreamTest.php index 73e2638..94a30db 100644 --- a/tests/unit/Tumblr/StreamBuilder/Streams/StreamTest.php +++ b/tests/unit/Tumblr/StreamBuilder/Streams/StreamTest.php @@ -99,68 +99,4 @@ public function test_empty_identity() ->setConstructorArgs(['']) ->getMockForAbstractClass(); } - - /** - * Test that the array_map function in enumerate method is executed - * This test verifies that the escaped mutant (removal of setComponent call) would be caught - */ - public function test_enumerate_executes_array_map_function() - { - // Create a mock stream that tracks if _enumerate was called - $stream = $this->getMockBuilder(Stream::class) - ->setConstructorArgs(['test_stream']) - ->setMethods(['_enumerate']) - ->getMockForAbstractClass(); - - // Create a mock element that tracks component setting - $mock_element = $this->getMockBuilder(\Tumblr\StreamBuilder\StreamElements\StreamElement::class) - ->disableOriginalConstructor() - ->getMock(); - - $mock_element->method('getComponent')->willReturn(null); - $mock_element->expects($this->once()) - ->method('setComponent') - ->with('test_component'); - - $stream->method('_enumerate') - ->willReturn(new \Tumblr\StreamBuilder\StreamResult(false, [$mock_element])); - - // Set the component on the stream - $stream->setComponent('test_component'); - - // Enumerate and verify the element's setComponent was called - $result = $stream->enumerate(1); - $this->assertCount(1, $result->get_elements()); - } - - /** - * Test that elements with existing components are not overridden - */ - public function test_enumerate_does_not_override_existing_component() - { - // Create a mock stream - $stream = $this->getMockBuilder(Stream::class) - ->setConstructorArgs(['test_stream']) - ->setMethods(['_enumerate']) - ->getMockForAbstractClass(); - - // Create a mock element that already has a component - $mock_element = $this->getMockBuilder(\Tumblr\StreamBuilder\StreamElements\StreamElement::class) - ->disableOriginalConstructor() - ->getMock(); - - $mock_element->method('getComponent')->willReturn('existing_component'); - $mock_element->expects($this->never()) - ->method('setComponent'); - - $stream->method('_enumerate') - ->willReturn(new \Tumblr\StreamBuilder\StreamResult(false, [$mock_element])); - - // Set a component on the stream - $stream->setComponent('test_component'); - - // Enumerate and verify setComponent was not called - $result = $stream->enumerate(1); - $this->assertCount(1, $result->get_elements()); - } } From 16c2f2ae70493642b56dda17e5627edd149df399 Mon Sep 17 00:00:00 2001 From: lucila Date: Mon, 27 Oct 2025 11:30:17 -0300 Subject: [PATCH 27/28] undo more unrelated tests --- .../StreamRankers/StreamRankerTest.php | 305 ------------------ .../StreamBuilder/StreamSerializerTest.php | 64 ---- 2 files changed, 369 deletions(-) delete mode 100644 tests/unit/Tumblr/StreamBuilder/StreamRankers/StreamRankerTest.php diff --git a/tests/unit/Tumblr/StreamBuilder/StreamRankers/StreamRankerTest.php b/tests/unit/Tumblr/StreamBuilder/StreamRankers/StreamRankerTest.php deleted file mode 100644 index 570d93e..0000000 --- a/tests/unit/Tumblr/StreamBuilder/StreamRankers/StreamRankerTest.php +++ /dev/null @@ -1,305 +0,0 @@ -pre_fetch_called = true; - $this->pre_fetch_elements = $elements; - } - - /** - * @param array $stream_elements The stream elements to rank - * @param StreamTracer|null $tracer Optional tracer for debugging - * @return array The ranked elements - */ - protected function rank_inner(array $stream_elements, ?StreamTracer $tracer = null): array - { - // Just return the elements in the same order - return $stream_elements; - } - - /** - * @return array The template array - */ - public function to_template(): array - { - return ['_type' => 'TestRanker']; - } - - /** - * @param \Tumblr\StreamBuilder\StreamContext $context The stream context - * @return self The created instance - */ - public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self - { - return new self('test_ranker'); - } - }; - - // Create test elements - $element1 = new MockedPostRefElement(1, 1); - $element2 = new MockedPostRefElement(2, 2); - $elements = [$element1, $element2]; - - // Call rank method - $result = $test_ranker->rank($elements); - - // Verify that pre_fetch was called - $this->assertTrue($test_ranker->pre_fetch_called); - $this->assertSame($elements, $test_ranker->pre_fetch_elements); - $this->assertSame($elements, $result); - } - - /** - * Test that pre_fetch is called with tracer during ranking - */ - public function test_pre_fetch_is_called_with_tracer_during_ranking(): void - { - // Create a test ranker that tracks pre_fetch calls - $test_ranker = new class('test_ranker') extends StreamRanker { - /** @var bool */ - public $pre_fetch_called = false; - /** @var array */ - public $pre_fetch_elements = []; - - /** - * @param array $elements The elements to pre-fetch - * @return void - */ - protected function pre_fetch(array $elements): void - { - $this->pre_fetch_called = true; - $this->pre_fetch_elements = $elements; - } - - /** - * @param array $stream_elements The stream elements to rank - * @param StreamTracer|null $tracer Optional tracer for debugging - * @return array The ranked elements - */ - protected function rank_inner(array $stream_elements, ?StreamTracer $tracer = null): array - { - // Just return the elements in the same order - return $stream_elements; - } - - /** - * @return array The template array - */ - public function to_template(): array - { - return ['_type' => 'TestRanker']; - } - - /** - * @param \Tumblr\StreamBuilder\StreamContext $context The stream context - * @return self The created instance - */ - public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self - { - return new self('test_ranker'); - } - }; - - // Create a mock tracer - $tracer = $this->getMockBuilder(StreamTracer::class) - ->disableOriginalConstructor() - ->getMock(); - - // Create test elements - $element1 = new MockedPostRefElement(1, 1); - $element2 = new MockedPostRefElement(2, 2); - $elements = [$element1, $element2]; - - // Call rank method with tracer - $result = $test_ranker->rank($elements, $tracer); - - // Verify that pre_fetch was called - $this->assertTrue($test_ranker->pre_fetch_called); - $this->assertSame($elements, $test_ranker->pre_fetch_elements); - $this->assertSame($elements, $result); - } - - /** - * Test that pre_fetch is called even when rank_inner throws an exception - */ - public function test_pre_fetch_is_called_even_when_rank_inner_throws_exception(): void - { - // Create a test ranker that tracks pre_fetch calls and throws exception - $test_ranker = new class('test_ranker') extends StreamRanker { - /** @var bool */ - public $pre_fetch_called = false; - /** @var array */ - public $pre_fetch_elements = []; - - /** - * @param array $elements The elements to pre-fetch - * @return void - */ - protected function pre_fetch(array $elements): void - { - $this->pre_fetch_called = true; - $this->pre_fetch_elements = $elements; - } - - /** - * @param array $stream_elements The stream elements to rank - * @param StreamTracer|null $tracer Optional tracer for debugging - * @return never Always throws exception - * @throws \Exception Always throws test exception - */ - protected function rank_inner(array $stream_elements, ?StreamTracer $tracer = null): array - { - // Throw an exception to test that pre_fetch is still called - throw new \Exception('Test exception'); - } - - /** - * @return array The template array - */ - public function to_template(): array - { - return ['_type' => 'TestRanker']; - } - - /** - * @param \Tumblr\StreamBuilder\StreamContext $context The stream context - * @return self The created instance - */ - public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self - { - return new self('test_ranker'); - } - }; - - // Create test elements - $element1 = new MockedPostRefElement(1, 1); - $element2 = new MockedPostRefElement(2, 2); - $elements = [$element1, $element2]; - - // Expect an exception to be thrown - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Test exception'); - - try { - $test_ranker->rank($elements); - } finally { - // Verify that pre_fetch was called even though an exception was thrown - $this->assertTrue($test_ranker->pre_fetch_called); - $this->assertSame($elements, $test_ranker->pre_fetch_elements); - } - } - - /** - * Test that pre_fetch is called with multiple elements - */ - public function test_pre_fetch_is_called_with_multiple_elements(): void - { - // Create a test ranker that tracks pre_fetch calls - $test_ranker = new class('test_ranker') extends StreamRanker { - /** @var bool */ - public $pre_fetch_called = false; - /** @var array */ - public $pre_fetch_elements = []; - - /** - * @param array $elements The elements to pre-fetch - * @return void - */ - protected function pre_fetch(array $elements): void - { - $this->pre_fetch_called = true; - $this->pre_fetch_elements = $elements; - } - - /** - * @param array $stream_elements The stream elements to rank - * @param StreamTracer|null $tracer Optional tracer for debugging - * @return array The ranked elements - */ - protected function rank_inner(array $stream_elements, ?StreamTracer $tracer = null): array - { - // Just return the elements in the same order - return $stream_elements; - } - - /** - * @return array The template array - */ - public function to_template(): array - { - return ['_type' => 'TestRanker']; - } - - /** - * @param \Tumblr\StreamBuilder\StreamContext $context The stream context - * @return self The created instance - */ - public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self - { - return new self('test_ranker'); - } - }; - - // Create multiple test elements - $elements = []; - for ($i = 1; $i <= 5; $i++) { - $elements[] = new MockedPostRefElement($i, $i); - } - - // Call rank method - $result = $test_ranker->rank($elements); - - // Verify that pre_fetch was called with all elements - $this->assertTrue($test_ranker->pre_fetch_called); - $this->assertSame($elements, $test_ranker->pre_fetch_elements); - $this->assertSame($elements, $result); - $this->assertCount(5, $test_ranker->pre_fetch_elements); - } -} diff --git a/tests/unit/Tumblr/StreamBuilder/StreamSerializerTest.php b/tests/unit/Tumblr/StreamBuilder/StreamSerializerTest.php index 977b9bd..549553f 100644 --- a/tests/unit/Tumblr/StreamBuilder/StreamSerializerTest.php +++ b/tests/unit/Tumblr/StreamBuilder/StreamSerializerTest.php @@ -129,68 +129,4 @@ public function testFromTemplateWithSkippedComponents(): void $this->assertFalse($filtered_stream->isSkippedComponent()); $this->assertTrue($filtered_stream->getInner()->isSkippedComponent()); } - - /** - * Test that component is set on object during deserialization - * This test verifies that the escaped mutant (removal of setComponent call) would be caught - */ - public function test_component_is_set_on_object_during_deserialization(): void - { - // Use a real class that can be instantiated - $template = [ - '_type' => NullStream::class, - StreamContext::COMPONENT_NAME => 'test_component', - ]; - - $context = new StreamContext($template, []); - $result = StreamSerializer::from_template($context); - - // Verify that the component was set - $this->assertSame('test_component', $result->getComponent()); - } - - /** - * Test that component is not set when no component is specified in template - */ - public function test_component_is_not_set_when_no_component_specified(): void - { - $template = ['_type' => NullStream::class]; - - $context = new StreamContext($template, []); - $result = StreamSerializer::from_template($context); - - // Verify that no component was set - $this->assertNull($result->getComponent()); - } - - /** - * Test that component is set to null when component is explicitly null - */ - public function test_component_is_set_to_null_when_explicitly_null(): void - { - $template = ['_type' => NullStream::class, StreamContext::COMPONENT_NAME => null]; - - $context = new StreamContext($template, []); - $result = StreamSerializer::from_template($context); - - // Verify that component was set to null - $this->assertNull($result->getComponent()); - } - - /** - * Test that component is set to empty string when component is empty string - */ - public function test_component_is_set_to_empty_string_when_empty(): void - { - $template = [ - '_type' => NullStream::class, - StreamContext::COMPONENT_NAME => '', - ]; - - $context = new StreamContext($template, []); - $result = StreamSerializer::from_template($context); - - // Verify that component was set to empty string - $this->assertSame('', $result->getComponent()); - } } From 38953e3d8ec4dc1420a39c8d03c1d862c6f4a143 Mon Sep 17 00:00:00 2001 From: lucila Date: Mon, 27 Oct 2025 11:34:38 -0300 Subject: [PATCH 28/28] remove unrelated changes --- .../StreamFilters/StreamElementFilterTest.php | 277 ------------------ 1 file changed, 277 deletions(-) delete mode 100644 tests/unit/Tumblr/StreamBuilder/StreamFilters/StreamElementFilterTest.php diff --git a/tests/unit/Tumblr/StreamBuilder/StreamFilters/StreamElementFilterTest.php b/tests/unit/Tumblr/StreamBuilder/StreamFilters/StreamElementFilterTest.php deleted file mode 100644 index 9c00642..0000000 --- a/tests/unit/Tumblr/StreamBuilder/StreamFilters/StreamElementFilterTest.php +++ /dev/null @@ -1,277 +0,0 @@ -pre_fetch_called = true; - $this->pre_fetch_elements = $elements; - } - - /** - * @param StreamElement $e The element to check - * @return bool Whether to release the element - */ - protected function should_release(StreamElement $e): bool - { - // Always retain elements for this test - return false; - } - - /** - * @return string|null The cache key - */ - public function get_cache_key(): ?string - { - return 'test_filter_cache_key'; - } - - /** - * @return array The template array - */ - public function to_template(): array - { - return ['_type' => 'TestFilter']; - } - - /** - * @param \Tumblr\StreamBuilder\StreamContext $context The stream context - * @return self The created instance - */ - public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self - { - return new self('test_filter'); - } - }; - - // Create test elements - $element1 = $this->getMockBuilder(StreamElement::class) - ->setConstructorArgs(['test1']) - ->getMockForAbstractClass(); - $element2 = $this->getMockBuilder(StreamElement::class) - ->setConstructorArgs(['test2']) - ->getMockForAbstractClass(); - - $elements = [$element1, $element2]; - - // Call filter method - $result = $test_filter->filter($elements); - - // Verify that pre_fetch was called - $this->assertTrue($test_filter->pre_fetch_called); - $this->assertSame($elements, $test_filter->pre_fetch_elements); - $this->assertSame($elements, $result->get_retained()); - $this->assertEmpty($result->get_released()); - } - - /** - * Test that pre_fetch is called with multiple elements - */ - public function test_pre_fetch_is_called_with_multiple_elements(): void - { - // Create a test filter that tracks pre_fetch calls - $test_filter = new class('test_filter') extends StreamElementFilter { - /** @var bool */ - public $pre_fetch_called = false; - /** @var array */ - public $pre_fetch_elements = []; - - /** - * @param array $elements The elements to pre-fetch - * @return void - */ - protected function pre_fetch(array $elements): void - { - $this->pre_fetch_called = true; - $this->pre_fetch_elements = $elements; - } - - /** - * @param StreamElement $e The element to check - * @return bool Whether to release the element - */ - protected function should_release(StreamElement $e): bool - { - // Always retain elements for this test - return false; - } - - /** - * @return string|null The cache key - */ - public function get_cache_key(): ?string - { - return 'test_filter_cache_key'; - } - - /** - * @return array The template array - */ - public function to_template(): array - { - return ['_type' => 'TestFilter']; - } - - /** - * @param \Tumblr\StreamBuilder\StreamContext $context The stream context - * @return self The created instance - */ - public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self - { - return new self('test_filter'); - } - }; - - // Create multiple test elements - $elements = []; - for ($i = 1; $i <= 5; $i++) { - $elements[] = $this->getMockBuilder(StreamElement::class) - ->setConstructorArgs(["test{$i}"]) - ->getMockForAbstractClass(); - } - - // Call filter method - $result = $test_filter->filter($elements); - - // Verify that pre_fetch was called with all elements - $this->assertTrue($test_filter->pre_fetch_called); - $this->assertSame($elements, $test_filter->pre_fetch_elements); - $this->assertSame($elements, $result->get_retained()); - $this->assertEmpty($result->get_released()); - $this->assertCount(5, $test_filter->pre_fetch_elements); - } - - /** - * Test that pre_fetch is called even when some elements are released - */ - public function test_pre_fetch_is_called_even_when_elements_are_released(): void - { - // Create a test filter that tracks pre_fetch calls - $test_filter = new class('test_filter') extends StreamElementFilter { - /** @var bool */ - public $pre_fetch_called = false; - /** @var array */ - public $pre_fetch_elements = []; - - /** - * @param array $elements The elements to pre-fetch - * @return void - */ - protected function pre_fetch(array $elements): void - { - $this->pre_fetch_called = true; - $this->pre_fetch_elements = $elements; - } - - /** - * @param StreamElement $e The element to check - * @return bool Whether to release the element - */ - protected function should_release(StreamElement $e): bool - { - // Release elements with '2' or '4' in their identity - return (strpos($e->get_element_id(), '2') !== false || strpos($e->get_element_id(), '4') !== false); - } - - /** - * @return string|null The cache key - */ - public function get_cache_key(): ?string - { - return 'test_filter_cache_key'; - } - - /** - * @return array The template array - */ - public function to_template(): array - { - return ['_type' => 'TestFilter']; - } - - /** - * @param \Tumblr\StreamBuilder\StreamContext $context The stream context - * @return self The created instance - */ - public static function from_template(\Tumblr\StreamBuilder\StreamContext $context): self - { - return new self('test_filter'); - } - }; - - // Create test elements - $element1 = $this->getMockBuilder(StreamElement::class) - ->setConstructorArgs(['test1']) - ->getMockForAbstractClass(); - $element2 = $this->getMockBuilder(StreamElement::class) - ->setConstructorArgs(['test2']) - ->getMockForAbstractClass(); - $element3 = $this->getMockBuilder(StreamElement::class) - ->setConstructorArgs(['test3']) - ->getMockForAbstractClass(); - $element4 = $this->getMockBuilder(StreamElement::class) - ->setConstructorArgs(['test4']) - ->getMockForAbstractClass(); - - // Mock the get_element_id method to return the identity - $element1->method('get_element_id')->willReturn('test1'); - $element2->method('get_element_id')->willReturn('test2'); - $element3->method('get_element_id')->willReturn('test3'); - $element4->method('get_element_id')->willReturn('test4'); - - $elements = [$element1, $element2, $element3, $element4]; - - // Call filter method - $result = $test_filter->filter($elements); - - // Verify that pre_fetch was called - $this->assertTrue($test_filter->pre_fetch_called); - $this->assertSame($elements, $test_filter->pre_fetch_elements); - $this->assertCount(2, $result->get_retained()); - $this->assertCount(2, $result->get_released()); - } -}