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;
+ }
}