diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9a077f9..2e55fc8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,7 +31,7 @@ jobs: - name: Execute tests run: composer test - type: + type_coverage: runs-on: ubuntu-latest name: Type Coverage @@ -51,4 +51,26 @@ jobs: run: composer update - name: Execute tests - run: composer lint:type-coverage + run: composer test:type-coverage + + coverage: + runs-on: ubuntu-latest + + name: Coverage + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: curl, mbstring, zip, pcntl, pdo, pdo_sqlite, iconv + coverage: xdebug + + - name: Install dependencies + run: composer update + + - name: Execute tests + run: composer test:coverage diff --git a/composer.json b/composer.json index bfcafd0..75dba04 100644 --- a/composer.json +++ b/composer.json @@ -70,11 +70,12 @@ ], "build": "@php vendor/bin/testbench workbench:build --ansi", "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", - "lint:type-coverage": "@php vendor/bin/pest --type-coverage --compact --min=95", "migrate": "@php vendor/bin/testbench migrate:fresh --seed --ansi", "prepare": "@php vendor/bin/testbench package:discover --ansi", "style": "vendor/bin/pint --parallel --ansi", "test": "@php vendor/bin/pest --parallel --colors=always", + "test:coverage": "@php vendor/bin/pest --colors=always --coverage --compact --parallel --min=95", + "test:type-coverage": "@php vendor/bin/pest --type-coverage --compact --min=95", "test:update": "@php vendor/bin/pest --colors=always --update-snapshots" } } diff --git a/src/Commands/FeedGenerateCommand.php b/src/Commands/FeedGenerateCommand.php index e62249c..481880d 100644 --- a/src/Commands/FeedGenerateCommand.php +++ b/src/Commands/FeedGenerateCommand.php @@ -49,7 +49,9 @@ protected function feedable(FeedQuery $feeds): array protected function messageYellow(string $message): string { if ($this->option('no-ansi')) { + // @codeCoverageIgnoreStart return $message; + // @codeCoverageIgnoreEnd } return $this->yellow($message); diff --git a/src/Exceptions/OpenFeedException.php b/src/Exceptions/OpenFeedException.php index 0b08ba4..637cf27 100644 --- a/src/Exceptions/OpenFeedException.php +++ b/src/Exceptions/OpenFeedException.php @@ -6,6 +6,7 @@ use RuntimeException; +// @codeCoverageIgnoreStart class OpenFeedException extends RuntimeException { public function __construct(string $path) @@ -13,3 +14,4 @@ public function __construct(string $path) parent::__construct("Unable to open file for writing: [$path]"); } } +// @codeCoverageIgnoreEnd diff --git a/src/Exceptions/WriteFeedException.php b/src/Exceptions/WriteFeedException.php index 87eb8ee..b8de9be 100644 --- a/src/Exceptions/WriteFeedException.php +++ b/src/Exceptions/WriteFeedException.php @@ -6,6 +6,7 @@ use RuntimeException; +// @codeCoverageIgnoreStart class WriteFeedException extends RuntimeException { public function __construct(string $path) @@ -13,3 +14,4 @@ public function __construct(string $path) parent::__construct("Failed to write to the feed: [$path]."); } } +// @codeCoverageIgnoreEnd diff --git a/src/Helpers/ClassExistsHelper.php b/src/Helpers/ClassExistsHelper.php index 007f984..db67134 100644 --- a/src/Helpers/ClassExistsHelper.php +++ b/src/Helpers/ClassExistsHelper.php @@ -6,6 +6,7 @@ use function class_exists; +// @codeCoverageIgnoreStart class ClassExistsHelper { public function exists(string $class): bool @@ -13,3 +14,4 @@ public function exists(string $class): bool return class_exists($class); } } +// @codeCoverageIgnoreEnd diff --git a/src/LaravelFeedServiceProvider.php b/src/LaravelFeedServiceProvider.php index e3cbb34..8dcd3f9 100644 --- a/src/LaravelFeedServiceProvider.php +++ b/src/LaravelFeedServiceProvider.php @@ -19,9 +19,11 @@ public function register(): void public function boot(): void { + // @codeCoverageIgnoreStart if (! $this->app->runningInConsole()) { return; } + // @codeCoverageIgnoreEnd $this->registerCommands(); $this->publishConfig(); diff --git a/src/Services/FilesystemService.php b/src/Services/FilesystemService.php index b618bcb..4e49940 100644 --- a/src/Services/FilesystemService.php +++ b/src/Services/FilesystemService.php @@ -34,7 +34,9 @@ public function open(string $path) // @pest-ignore-type $resource = fopen($path, 'ab'); if ($resource === false) { + // @codeCoverageIgnoreStart throw new OpenFeedException($path); + // @codeCoverageIgnoreEnd } return $resource; @@ -50,7 +52,9 @@ public function append($resource, string $content, string $path): void // @pest- } if (fwrite($resource, $content) === false) { + // @codeCoverageIgnoreStart throw new WriteFeedException($path); + // @codeCoverageIgnoreEnd } } @@ -77,7 +81,9 @@ public function release($resource, string $path): void // @pest-ignore-type public function close($resource): void // @pest-ignore-type { if (! is_resource($resource)) { + // @codeCoverageIgnoreStart return; + // @codeCoverageIgnoreEnd } fclose($resource); diff --git a/tests/.pest/snapshots/Feature/Feeds/FullTest/export_with_data_set____false__.snap b/tests/.pest/snapshots/Feature/Feeds/FullTest/export_with_data_set____false__.snap index 10fc567..5f50b3e 100644 --- a/tests/.pest/snapshots/Feature/Feeds/FullTest/export_with_data_set____false__.snap +++ b/tests/.pest/snapshots/Feature/Feeds/FullTest/export_with_data_set____false__.snap @@ -1,6 +1,12 @@ -[NEWS]:Some 1Some content 1Some extra dataLuke SkywalkerLightsaberSauron]]>Evil Eye -[NEWS]:Some 2Some content 2Some extra dataLuke SkywalkerLightsaberSauron]]>Evil Eye -[NEWS]:Some 3Some content 3Some extra dataLuke SkywalkerLightsaberSauron]]>Evil Eye +[NEWS]:Some 1Some content 1Some extra dataLuke SkywalkerLightsaberSauron]]>Evil Eyeline +line with some html/xml tag +line with & symbol +[NEWS]:Some 2Some content 2Some extra dataLuke SkywalkerLightsaberSauron]]>Evil Eyeline +line with some html/xml tag +line with & symbol +[NEWS]:Some 3Some content 3Some extra dataLuke SkywalkerLightsaberSauron]]>Evil Eyeline +line with some html/xml tag +line with & symbol diff --git a/tests/.pest/snapshots/Feature/Feeds/FullTest/export_with_data_set____true__.snap b/tests/.pest/snapshots/Feature/Feeds/FullTest/export_with_data_set____true__.snap index 8b7a43e..2b588c6 100644 --- a/tests/.pest/snapshots/Feature/Feeds/FullTest/export_with_data_set____true__.snap +++ b/tests/.pest/snapshots/Feature/Feeds/FullTest/export_with_data_set____true__.snap @@ -14,6 +14,9 @@ Evil Eye + line +line with some html/xml tag +line with & symbol [NEWS]:Some 2 @@ -29,6 +32,9 @@ Evil Eye + line +line with some html/xml tag +line with & symbol [NEWS]:Some 3 @@ -44,5 +50,8 @@ Evil Eye + line +line with some html/xml tag +line with & symbol diff --git a/tests/.pest/snapshots/Feature/Feeds/ModelTest/export_with_data_set____false__.snap b/tests/.pest/snapshots/Feature/Feeds/ModelTest/export_with_data_set____false__.snap new file mode 100644 index 0000000..d806987 --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/ModelTest/export_with_data_set____false__.snap @@ -0,0 +1,3 @@ +1Some 1Some content 12025-09-04T04:08:12.000000Z2025-09-04T04:08:12.000000Z +2Some 2Some content 22025-09-04T04:08:12.000000Z2025-09-04T04:08:12.000000Z +3Some 3Some content 32025-09-04T04:08:12.000000Z2025-09-04T04:08:12.000000Z \ No newline at end of file diff --git a/tests/.pest/snapshots/Feature/Feeds/ModelTest/export_with_data_set____true__.snap b/tests/.pest/snapshots/Feature/Feeds/ModelTest/export_with_data_set____true__.snap new file mode 100644 index 0000000..83c9001 --- /dev/null +++ b/tests/.pest/snapshots/Feature/Feeds/ModelTest/export_with_data_set____true__.snap @@ -0,0 +1,21 @@ + + 1 + Some 1 + Some content 1 + 2025-09-04T04:08:12.000000Z + 2025-09-04T04:08:12.000000Z + + + 2 + Some 2 + Some content 2 + 2025-09-04T04:08:12.000000Z + 2025-09-04T04:08:12.000000Z + + + 3 + Some 3 + Some content 3 + 2025-09-04T04:08:12.000000Z + 2025-09-04T04:08:12.000000Z + \ No newline at end of file diff --git a/tests/.pest/snapshots/Feature/Feeds/PartialTest/export_with_data_set____false__.snap b/tests/.pest/snapshots/Feature/Feeds/PartialTest/export_with_data_set____false__.snap index 9931cdf..0e2f102 100644 --- a/tests/.pest/snapshots/Feature/Feeds/PartialTest/export_with_data_set____false__.snap +++ b/tests/.pest/snapshots/Feature/Feeds/PartialTest/export_with_data_set____false__.snap @@ -1,6 +1,12 @@ -[NEWS]:Some 1Some content 1Some extra dataLuke SkywalkerLightsaberSauron]]>Evil Eye -[NEWS]:Some 2Some content 2Some extra dataLuke SkywalkerLightsaberSauron]]>Evil Eye -[NEWS]:Some 3Some content 3Some extra dataLuke SkywalkerLightsaberSauron]]>Evil Eye +[NEWS]:Some 1Some content 1Some extra dataLuke SkywalkerLightsaberSauron]]>Evil Eyeline +line with some html/xml tag +line with & symbol +[NEWS]:Some 2Some content 2Some extra dataLuke SkywalkerLightsaberSauron]]>Evil Eyeline +line with some html/xml tag +line with & symbol +[NEWS]:Some 3Some content 3Some extra dataLuke SkywalkerLightsaberSauron]]>Evil Eyeline +line with some html/xml tag +line with & symbol diff --git a/tests/.pest/snapshots/Feature/Feeds/PartialTest/export_with_data_set____true__.snap b/tests/.pest/snapshots/Feature/Feeds/PartialTest/export_with_data_set____true__.snap index 181d385..4fe838a 100644 --- a/tests/.pest/snapshots/Feature/Feeds/PartialTest/export_with_data_set____true__.snap +++ b/tests/.pest/snapshots/Feature/Feeds/PartialTest/export_with_data_set____true__.snap @@ -14,6 +14,9 @@ Evil Eye + line +line with some html/xml tag +line with & symbol [NEWS]:Some 2 @@ -29,6 +32,9 @@ Evil Eye + line +line with some html/xml tag +line with & symbol [NEWS]:Some 3 @@ -44,5 +50,8 @@ Evil Eye + line +line with some html/xml tag +line with & symbol diff --git a/tests/Expectations.php b/tests/Expectations.php index 26d9f96..2f3a677 100644 --- a/tests/Expectations.php +++ b/tests/Expectations.php @@ -41,3 +41,17 @@ return $this; }); + +expect()->pipe('toMatchSnapshot', function (Closure $next) { + if (! is_string($this->value)) { + return $this->value; + } + + $this->value = preg_replace( + pattern : '/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{6}Z)/', + replacement: '2025-09-04T04:08:12.000000Z', + subject : $this->value + ); + + return $next(); +}); diff --git a/tests/Feature/Console/Generation/EnsureDeleteTest.php b/tests/Feature/Console/Generation/EnsureDeleteTest.php new file mode 100644 index 0000000..66d0040 --- /dev/null +++ b/tests/Feature/Console/Generation/EnsureDeleteTest.php @@ -0,0 +1,29 @@ +class)->path(); + $draft = app($feed->class)->path() . '.draft'; + + $filesystem->ensureDirectoryExists(dirname($path)); + + $filesystem->put($path, 'foo'); + $filesystem->put($draft, 'bar'); + + artisan(FeedGenerateCommand::class, [ + 'feed' => $feed->id, + ])->assertSuccessful()->run(); + + expect($feed)->toMatchGeneratedFeed(); +}); diff --git a/tests/Feature/Console/Schedule/RegisterTest.php b/tests/Feature/Console/Schedule/RegisterTest.php new file mode 100644 index 0000000..1165cf4 --- /dev/null +++ b/tests/Feature/Console/Schedule/RegisterTest.php @@ -0,0 +1,34 @@ +toHaveCount(0); + + app(ScheduleFeedHelper::class)->commands(); + + $feeds = Feed::get(); + + $events = collect(Schedule::events()) + ->mapWithKeys(function (Event $event) { + $key = Str::of($event->command)->afterLast(' ')->toInteger(); + $value = $event->expression; + + return [$key => $value]; + }) + ->all(); + + expect($events)->toHaveCount( + $feeds->count() + ); + + $feeds->each( + fn (Feed $feed) => expect($events[$feed->id])->toBe($feed->expression) + ); +}); diff --git a/tests/Feature/Feeds/ModelTest.php b/tests/Feature/Feeds/ModelTest.php new file mode 100644 index 0000000..2341480 --- /dev/null +++ b/tests/Feature/Feeds/ModelTest.php @@ -0,0 +1,14 @@ +with('boolean'); diff --git a/tests/Feature/Feeds/PartialTest.php b/tests/Feature/Feeds/PartialTest.php index aa0b0ca..39fd99a 100644 --- a/tests/Feature/Feeds/PartialTest.php +++ b/tests/Feature/Feeds/PartialTest.php @@ -9,7 +9,7 @@ setPrettyXml($pretty); createNews(static fn () => [ - 'updated_at' => fake()->dateTimeBetween(endDate: '-1 month'), + 'updated_at' => fake()->dateTimeBetween(endDate: getDefaultDateTime()->subMonth()->toIso8601String()), ]); createNews(...NewsFakeData::toArray()); diff --git a/tests/Feature/Queries/Delete/SuccessTest.php b/tests/Feature/Queries/Delete/SuccessTest.php new file mode 100644 index 0000000..cf9e2a3 --- /dev/null +++ b/tests/Feature/Queries/Delete/SuccessTest.php @@ -0,0 +1,29 @@ + $feed->id, + 'deleted_at' => null, + ]); + + app(FeedQuery::class)->delete($feed->id); + + assertDatabaseHas(Feed::class, [ + 'id' => $feed->id, + ]); + + assertDatabaseMissing(Feed::class, [ + 'id' => $feed->id, + 'deleted_at' => null, + ]); +}); diff --git a/tests/Feature/Queries/Delete/UnknownIdentifierTest.php b/tests/Feature/Queries/Delete/UnknownIdentifierTest.php new file mode 100644 index 0000000..8246a67 --- /dev/null +++ b/tests/Feature/Queries/Delete/UnknownIdentifierTest.php @@ -0,0 +1,16 @@ +delete(1000); + + assertDatabaseMissing(Feed::class, [ + 'id' => 1000, + ]); +}); diff --git a/tests/Feature/Queries/Restore/SuccessTest.php b/tests/Feature/Queries/Restore/SuccessTest.php new file mode 100644 index 0000000..e54beb8 --- /dev/null +++ b/tests/Feature/Queries/Restore/SuccessTest.php @@ -0,0 +1,25 @@ +delete(); + + assertDatabaseHas(Feed::class, [ + 'id' => $feed->id, + 'deleted_at' => $feed->deleted_at->toDateTimeString(), + ]); + + app(FeedQuery::class)->restore($feed->id); + + assertDatabaseHas(Feed::class, [ + 'id' => $feed->id, + 'deleted_at' => null, + ]); +}); diff --git a/tests/Feature/Queries/Restore/UnknownIdentifierTest.php b/tests/Feature/Queries/Restore/UnknownIdentifierTest.php new file mode 100644 index 0000000..3e90410 --- /dev/null +++ b/tests/Feature/Queries/Restore/UnknownIdentifierTest.php @@ -0,0 +1,16 @@ +restore(1000); + + assertDatabaseMissing(Feed::class, [ + 'id' => 1000, + ]); +}); diff --git a/tests/Helpers/dates.php b/tests/Helpers/dates.php new file mode 100644 index 0000000..022235d --- /dev/null +++ b/tests/Helpers/dates.php @@ -0,0 +1,15 @@ +use(RefreshDatabase::class) ->in('Feature') ->beforeEach(function () { + setDefaultDateTime(); + mockOperations(); mockPaths(); @@ -30,7 +31,7 @@ ->extend(TestCase::class) ->in('Unit') ->beforeEach(function () { - Carbon::setTestNow('2025-09-03 01:50:24'); + setDefaultDateTime(); mockOperations(); mockPaths(); diff --git a/workbench/app/Data/NewsFakeData.php b/workbench/app/Data/NewsFakeData.php index cb72494..7adbb24 100644 --- a/workbench/app/Data/NewsFakeData.php +++ b/workbench/app/Data/NewsFakeData.php @@ -10,20 +10,42 @@ public static function toArray(): array { return [ [ - 'title' => 'Some 1', - 'content' => 'Some content 1', - 'updated_at' => fake()->dateTimeBetween(startDate: '-23 hours'), + 'title' => 'Some 1', + 'content' => 'Some content 1', + + 'updated_at' => fake()->dateTimeBetween( + startDate: static::startDate(), + endDate : static::endDate() + ), ], [ - 'title' => 'Some 2', - 'content' => 'Some content 2', - 'updated_at' => fake()->dateTimeBetween(startDate: '-23 hours'), + 'title' => 'Some 2', + 'content' => 'Some content 2', + + 'updated_at' => fake()->dateTimeBetween( + startDate: static::startDate(), + endDate : static::endDate() + ), ], [ - 'title' => 'Some 3', - 'content' => 'Some content 3', - 'updated_at' => fake()->dateTimeBetween(startDate: '-23 hours'), + 'title' => 'Some 3', + 'content' => 'Some content 3', + + 'updated_at' => fake()->dateTimeBetween( + startDate: static::startDate(), + endDate : static::endDate() + ), ], ]; } + + protected static function startDate(): string + { + return getDefaultDateTime()->subHours(23)->toIso8601String(); + } + + protected static function endDate(): string + { + return getDefaultDateTime()->toIso8601String(); + } } diff --git a/workbench/app/Feeds/Items/NewsFeedItem.php b/workbench/app/Feeds/Items/NewsFeedItem.php index d0793db..16f8a5a 100644 --- a/workbench/app/Feeds/Items/NewsFeedItem.php +++ b/workbench/app/Feeds/Items/NewsFeedItem.php @@ -36,6 +36,14 @@ public function toArray(): array 'weapon' => 'Evil Eye', ], ], + + 'with mixed' => [ + '@mixed' => <<<'XML' + line + line with some html/xml tag + line with & symbol + XML, + ], ]; } } diff --git a/workbench/app/Feeds/ModelFeed.php b/workbench/app/Feeds/ModelFeed.php new file mode 100644 index 0000000..fd1fc0f --- /dev/null +++ b/workbench/app/Feeds/ModelFeed.php @@ -0,0 +1,17 @@ + $name, 'title' => $name, + + 'expression' => $this->expression(), + ]); + } + + protected function expression(): string + { + return implode(' ', [ + $this->protectedMinute(), + fake()->numberBetween(0, 23), + fake()->numberBetween(1, 10), + fake()->numberBetween(1, 12), + fake()->numberBetween(2, 6), ]); } + + protected function protectedMinute(): int + { + $value = fake()->numberBetween(0, 59); + + if ($value === Carbon::now()->minute) { + return $this->protectedMinute(); + } + + return $value; + } }