diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c02b873..d99ab21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,23 @@ jobs: - uses: ramsey/composer-install@v3 - run: composer phpunit + code-coverage: + name: Code Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + coverage: pcov + - uses: ramsey/composer-install@v3 + - name: Generate coverage report + run: vendor/bin/phpunit --coverage-clover=coverage.xml + - name: Upload to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + static-analysis: name: Static Analysis runs-on: ubuntu-latest @@ -26,4 +43,5 @@ jobs: with: php-version: '8.5' - uses: ramsey/composer-install@v3 + - run: composer phpcs - run: composer phpstan diff --git a/composer.json b/composer.json index daaa710..e47b065 100644 --- a/composer.json +++ b/composer.json @@ -40,6 +40,7 @@ } }, "scripts": { + "coverage": "vendor/bin/phpunit --coverage-text", "phpcs": "vendor/bin/phpcs", "phpstan": "vendor/bin/phpstan analyze", "phpunit": "vendor/bin/phpunit", diff --git a/tests/AbstractMapperTest.php b/tests/AbstractMapperTest.php index 29ea259..fd7cfbd 100644 --- a/tests/AbstractMapperTest.php +++ b/tests/AbstractMapperTest.php @@ -9,6 +9,10 @@ use PHPUnit\Framework\TestCase; use ReflectionObject; use Respect\Data\Collections\Collection; +use Respect\Data\Styles\CakePHP; +use Respect\Data\Styles\Standard; +use SplObjectStorage; +use stdClass; #[CoversClass(AbstractMapper::class)] class AbstractMapperTest extends TestCase @@ -82,4 +86,130 @@ public function magicGetterShouldBypassToCollection(): void $expected->setMapper($this->mapper); $this->assertEquals($expected->bar->baz, $collection); } + + #[Test] + public function getStyleShouldReturnDefaultStandard(): void + { + $style = $this->mapper->getStyle(); + $this->assertInstanceOf(Standard::class, $style); + } + + #[Test] + public function getStyleShouldReturnSameInstanceOnSubsequentCalls(): void + { + $style1 = $this->mapper->getStyle(); + $style2 = $this->mapper->getStyle(); + $this->assertSame($style1, $style2); + } + + #[Test] + public function setStyleShouldChangeStyle(): void + { + $style = new CakePHP(); + $result = $this->mapper->setStyle($style); + $this->assertSame($style, $this->mapper->getStyle()); + $this->assertSame($this->mapper, $result); + } + + #[Test] + public function persistShouldMarkObjectAsTracked(): void + { + $entity = new stdClass(); + $collection = Collection::foo(); + $this->mapper->persist($entity, $collection); + $this->assertTrue($this->mapper->isTracked($entity)); + } + + #[Test] + public function persistAlreadyTrackedShouldReturnTrue(): void + { + $entity = new stdClass(); + $collection = Collection::foo(); + $this->mapper->markTracked($entity, $collection); + $result = $this->mapper->persist($entity, $collection); + $this->assertTrue($result); + } + + #[Test] + public function removeShouldMarkObjectAsTracked(): void + { + $entity = new stdClass(); + $collection = Collection::foo(); + $result = $this->mapper->remove($entity, $collection); + $this->assertTrue($result); + $this->assertTrue($this->mapper->isTracked($entity)); + } + + #[Test] + public function removeAlreadyTrackedShouldReturnTrue(): void + { + $entity = new stdClass(); + $collection = Collection::foo(); + $this->mapper->markTracked($entity, $collection); + $result = $this->mapper->remove($entity, $collection); + $this->assertTrue($result); + } + + #[Test] + public function isTrackedShouldReturnFalseForUntrackedEntity(): void + { + $this->assertFalse($this->mapper->isTracked(new stdClass())); + } + + #[Test] + public function markTrackedShouldReturnTrue(): void + { + $entity = new stdClass(); + $collection = Collection::foo(); + $this->assertTrue($this->mapper->markTracked($entity, $collection)); + } + + #[Test] + public function resetShouldClearChangedRemovedAndNew(): void + { + $entity = new stdClass(); + $collection = Collection::foo(); + $this->mapper->persist($entity, $collection); + $this->mapper->remove($entity, $collection); + $this->mapper->reset(); + + $ref = new ReflectionObject($this->mapper); + + $newProp = $ref->getProperty('new'); + /** @var SplObjectStorage $newStorage */ + $newStorage = $newProp->getValue($this->mapper); + $this->assertCount(0, $newStorage); + + $changedProp = $ref->getProperty('changed'); + /** @var SplObjectStorage $changedStorage */ + $changedStorage = $changedProp->getValue($this->mapper); + $this->assertCount(0, $changedStorage); + + $removedProp = $ref->getProperty('removed'); + /** @var SplObjectStorage $removedStorage */ + $removedStorage = $removedProp->getValue($this->mapper); + $this->assertCount(0, $removedStorage); + } + + #[Test] + public function issetShouldReturnTrueForRegisteredCollection(): void + { + $coll = Collection::foo(); + $this->mapper->registerCollection('my_alias', $coll); + $this->assertTrue(isset($this->mapper->my_alias)); + } + + #[Test] + public function issetShouldReturnFalseForUnregisteredCollection(): void + { + $this->assertFalse(isset($this->mapper->nonexistent)); + } + + #[Test] + public function magicGetShouldReturnNewCollectionWhenNotRegistered(): void + { + $coll = $this->mapper->unregistered; + $this->assertInstanceOf(Collection::class, $coll); + $this->assertEquals('unregistered', $coll->getName()); + } } diff --git a/tests/CollectionIteratorTest.php b/tests/CollectionIteratorTest.php index be84d73..04429ca 100644 --- a/tests/CollectionIteratorTest.php +++ b/tests/CollectionIteratorTest.php @@ -89,4 +89,12 @@ public function getChildrenUseCollectionNext(): void $iterator = new CollectionIterator($coll); $this->assertTrue($iterator->hasChildren()); } + + #[Test] + public function recursiveTraversalShouldVisitNextChain(): void + { + $coll = Collection::foo()->bar->baz; + $items = iterator_to_array(CollectionIterator::recursive($coll)); + $this->assertCount(3, $items); + } } diff --git a/tests/Collections/CollectionTest.php b/tests/Collections/CollectionTest.php index a70d4c9..8986c30 100644 --- a/tests/Collections/CollectionTest.php +++ b/tests/Collections/CollectionTest.php @@ -297,4 +297,85 @@ public function fetchAllOnCollectionShouldExceptionIfMapperDontExist(): void $this->expectException(RuntimeException::class); Collection::foo()->fetchAll(); } + + #[Test] + public function usingShouldCreateCollectionWithCondition(): void + { + $coll = Collection::using(42); + $this->assertInstanceOf(Collection::class, $coll); + $this->assertEquals(42, $coll->getCondition()); + } + + #[Test] + public function haveShouldReturnFalseForMissingExtra(): void + { + $coll = new Collection('foo'); + $this->assertFalse($coll->have('nonexistent')); + } + + #[Test] + public function haveShouldReturnTrueForExistingExtra(): void + { + $coll = new Collection('foo'); + $coll->extra('key', 'value'); + $this->assertTrue($coll->have('key')); + } + + #[Test] + public function getExtraShouldReturnNullForMissingExtra(): void + { + $coll = new Collection('foo'); + $this->assertNull($coll->getExtra('nonexistent')); + } + + #[Test] + public function getExtraShouldReturnValueForExistingExtra(): void + { + $coll = new Collection('foo'); + $coll->extra('key', 'value'); + $this->assertEquals('value', $coll->getExtra('key')); + } + + #[Test] + public function extraShouldReturnSelf(): void + { + $coll = new Collection('foo'); + $result = $coll->extra('key', 'value'); + $this->assertSame($coll, $result); + } + + #[Test] + public function getParentNameShouldReturnNullWhenNoParent(): void + { + $coll = new Collection('foo'); + $this->assertNull($coll->getParentName()); + } + + #[Test] + public function getNextNameShouldReturnNullWhenNoNext(): void + { + $coll = new Collection('foo'); + $this->assertNull($coll->getNextName()); + } + + #[Test] + public function hasNextShouldReturnFalseWhenNoNext(): void + { + $coll = new Collection('foo'); + $this->assertFalse($coll->hasNext()); + } + + #[Test] + public function magicGetShouldUseRegisteredCollectionFromMapper(): void + { + $registered = Collection::bar(); + $mapperMock = $this->createMock(AbstractMapper::class); + $mapperMock->method('__isset')->with('bar')->willReturn(true); + $mapperMock->method('__get')->with('bar')->willReturn($registered); + + $coll = new Collection('foo'); + $coll->setMapper($mapperMock); + $result = $coll->bar; + $this->assertEquals('bar', $result->getNextName()); + } } diff --git a/tests/Collections/FilteredTest.php b/tests/Collections/FilteredTest.php index 69331e3..3fdc4bd 100644 --- a/tests/Collections/FilteredTest.php +++ b/tests/Collections/FilteredTest.php @@ -28,4 +28,13 @@ public function collectionCanBeCreatedStaticallyWithChildren(): void $this->assertEquals(['bar'], $children1->getExtra('filters')); $this->assertEquals(['bat'], $children2->getExtra('filters')); } + + #[Test] + public function callStaticShouldCreateFilteredCollectionWithName(): void + { + $coll = Filtered::items(); + $this->assertInstanceOf(Filtered::class, $coll); + $this->assertEquals('items', $coll->getName()); + $this->assertEquals([], $coll->getExtra('filters')); + } } diff --git a/tests/Collections/MixedTest.php b/tests/Collections/MixedTest.php index 7205ded..6bee40e 100644 --- a/tests/Collections/MixedTest.php +++ b/tests/Collections/MixedTest.php @@ -28,4 +28,13 @@ public function collectionCanBeCreatedStaticallyWithChildren(): void $this->assertEquals(['foo' => ['bar']], $children1->getExtra('mixins')); $this->assertEquals(['bat' => ['bar']], $children2->getExtra('mixins')); } + + #[Test] + public function callStaticShouldCreateMixCollectionWithName(): void + { + $coll = Mix::items(); + $this->assertInstanceOf(Mix::class, $coll); + $this->assertEquals('items', $coll->getName()); + $this->assertEquals([], $coll->getExtra('mixins')); + } } diff --git a/tests/Collections/TypedTest.php b/tests/Collections/TypedTest.php index f95f56d..5d65c3e 100644 --- a/tests/Collections/TypedTest.php +++ b/tests/Collections/TypedTest.php @@ -28,4 +28,13 @@ public function collectionCanBeCreatedStaticallyWithChildren(): void $this->assertEquals('a', $children1->getExtra('type')); $this->assertEquals('b', $children2->getExtra('type')); } + + #[Test] + public function callStaticShouldCreateTypedCollectionWithName(): void + { + $coll = Typed::items(); + $this->assertInstanceOf(Typed::class, $coll); + $this->assertEquals('items', $coll->getName()); + $this->assertEquals('', $coll->getExtra('type')); + } } diff --git a/tests/Styles/AbstractStyleTest.php b/tests/Styles/AbstractStyleTest.php index d45c541..e962f96 100644 --- a/tests/Styles/AbstractStyleTest.php +++ b/tests/Styles/AbstractStyleTest.php @@ -114,4 +114,16 @@ public function testCamelCaseToSeparatorAndViceVersa( $separatorToCamelCaseMethod->invoke($this->style, $separated, $separator), ); } + + public function testPluralToSingularReturnsUnchangedWhenNoMatch(): void + { + $method = new ReflectionMethod($this->style, 'pluralToSingular'); + $this->assertEquals('fox', $method->invoke($this->style, 'fox')); + } + + public function testSingularToPluralReturnsUnchangedWhenNoMatch(): void + { + $method = new ReflectionMethod($this->style, 'singularToPlural'); + $this->assertEquals('news', $method->invoke($this->style, 'news')); + } } diff --git a/tests/Styles/CakePHP/CakePHPIntegrationTest.php b/tests/Styles/CakePHP/CakePHPIntegrationTest.php index e9b881e..0620ac2 100644 --- a/tests/Styles/CakePHP/CakePHPIntegrationTest.php +++ b/tests/Styles/CakePHP/CakePHPIntegrationTest.php @@ -9,7 +9,6 @@ use Respect\Data\InMemoryMapper; use Respect\Data\Styles\CakePHP; -#[CoversClass(InMemoryMapper::class)] #[CoversClass(CakePHP::class)] class CakePHPIntegrationTest extends TestCase { diff --git a/tests/Styles/NorthWind/NorthWindIntegrationTest.php b/tests/Styles/NorthWind/NorthWindIntegrationTest.php index 512a371..948fb4d 100644 --- a/tests/Styles/NorthWind/NorthWindIntegrationTest.php +++ b/tests/Styles/NorthWind/NorthWindIntegrationTest.php @@ -9,7 +9,6 @@ use Respect\Data\InMemoryMapper; use Respect\Data\Styles\NorthWind; -#[CoversClass(InMemoryMapper::class)] #[CoversClass(NorthWind::class)] class NorthWindIntegrationTest extends TestCase { diff --git a/tests/Styles/Plural/PluralIntegrationTest.php b/tests/Styles/Plural/PluralIntegrationTest.php index 0911a5e..14521db 100644 --- a/tests/Styles/Plural/PluralIntegrationTest.php +++ b/tests/Styles/Plural/PluralIntegrationTest.php @@ -9,7 +9,6 @@ use Respect\Data\InMemoryMapper; use Respect\Data\Styles\Plural; -#[CoversClass(InMemoryMapper::class)] #[CoversClass(Plural::class)] class PluralIntegrationTest extends TestCase { diff --git a/tests/Styles/Sakila/SakilaIntegrationTest.php b/tests/Styles/Sakila/SakilaIntegrationTest.php index ed52e8f..5c1422b 100644 --- a/tests/Styles/Sakila/SakilaIntegrationTest.php +++ b/tests/Styles/Sakila/SakilaIntegrationTest.php @@ -9,7 +9,6 @@ use Respect\Data\InMemoryMapper; use Respect\Data\Styles\Sakila; -#[CoversClass(InMemoryMapper::class)] #[CoversClass(Sakila::class)] class SakilaIntegrationTest extends TestCase {