diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
index e68c452..6b2e33d 100644
--- a/.github/workflows/security.yml
+++ b/.github/workflows/security.yml
@@ -36,7 +36,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- extensions: dom, sockets, grpc, curl
+ extensions: pcntl
- name: Check Out Code
uses: actions/checkout@v4
diff --git a/.github/workflows/tests-chokidar.yml b/.github/workflows/tests-chokidar.yml
new file mode 100644
index 0000000..5a4fcac
--- /dev/null
+++ b/.github/workflows/tests-chokidar.yml
@@ -0,0 +1,70 @@
+name: 'Tests with Chokidar'
+
+on:
+ push:
+ branches:
+ - main
+ paths-ignore:
+ - '.gitignore'
+ - 'CHANGELOG.md'
+ - 'LICENSE'
+ - 'README.md'
+
+ pull_request:
+ paths-ignore:
+ - '.gitignore'
+ - 'CHANGELOG.md'
+ - 'LICENSE'
+ - 'README.md'
+
+jobs:
+ tests:
+ timeout-minutes: 5
+ name: Tests with Chokidar (PHP ${{ matrix.php }}, OS ${{ matrix.os }})
+
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ matrix:
+ os: [ubuntu-latest]
+ php: [8.1, 8.2, 8.3, 8.4]
+
+ env:
+ extensions: xdebug
+
+ steps:
+ - name: Check Out Code
+ uses: actions/checkout@v4
+
+ - name: Remove fswatch
+ run: sudo apt-get remove fswatch
+
+ - name: Setup node
+ uses: actions/setup-node@v6
+
+ - name: Install chokidar
+ run: npm install chokidar
+
+ - name: Setup PHP ${{ matrix.php }}
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: pcntl
+
+ - name: Validate composer.json and composer.lock
+ run: composer validate --strict
+
+ - name: Install dependencies with composer
+ uses: ramsey/composer-install@v3
+ with:
+ dependency-versions: ${{ matrix.dependencies }}
+
+ - name: Validate lowest dependencies
+ if: matrix.dependencies == 'lowest' && matrix.php == '8.1'
+ env:
+ COMPOSER_POOL_OPTIMIZER: 0
+ run: vendor/bin/validate-prefer-lowest
+
+ - name: Run tests with Phpunit
+ run: |
+ XDEBUG_MODE=coverage php vendor/bin/phpunit
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests-fswatch.yml
similarity index 79%
rename from .github/workflows/tests.yml
rename to .github/workflows/tests-fswatch.yml
index 072ad5f..609fe38 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests-fswatch.yml
@@ -1,4 +1,4 @@
-name: 'Tests'
+name: 'Tests with FsWatch'
on:
push:
@@ -18,16 +18,16 @@ on:
- 'README.md'
jobs:
- tests:
- name: Tests (PHP ${{ matrix.php }}, OS ${{ matrix.os }})
+ tests-fswatch:
+ timeout-minutes: 5
+ name: Tests with FsWatch (PHP ${{ matrix.php }}, OS ${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ubuntu-latest]
+ os: [macos-latest ]
php: [8.1, 8.2, 8.3, 8.4]
-
env:
extensions: xdebug
@@ -35,11 +35,14 @@ jobs:
- name: Check Out Code
uses: actions/checkout@v4
+ - name: Install fswatch
+ run: brew install fswatch
+
- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- extensions: dom, sockets, grpc, curl ${{ matrix.extensions-suffix }}
+ extensions: pcntl
- name: Validate composer.json and composer.lock
run: composer validate --strict
@@ -57,4 +60,4 @@ jobs:
- name: Run tests with Phpunit
run: |
- XDEBUG_MODE=coverage php vendor/bin/phpunit --testsuite unit
+ php vendor/bin/phpunit
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 2b30dbf..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-language: php
-
-jobs:
- include:
- - stage: "PHP7.4 - lowest"
- php: 7.4
- script:
- - composer update -n --prefer-dist --no-suggest
- - composer dump-autoload
- - composer ci:tests
- - composer ci:php:psalm
-
- - stage: "PHP8.0 - highest"
- php: 8.0
- script:
- - composer update -n --prefer-dist --no-suggest
- - composer dump-autoload
- - composer ci:tests
- - composer ci:php:psalm
diff --git a/README.md b/README.md
index 3905446..864bcf8 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,7 @@ PHP-watcher does not require any additional changes to your code or method of
* [Default executable](#default-executable)
* [Gracefully reloading down your script](#gracefully-reloading-down-your-script)
* [Automatic restart](#automatic-restart)
+* [Performance](#performance)
* [Spinner](#spinner)
## Installation
@@ -55,9 +56,6 @@ composer require seregazhuk/php-watcher --dev
```
Locally installed you can run it with `vendor/bin/php-watcher`.
-Under the hood, to watch filesystem changes, PHP-watcher uses JavaScript package [chokidar](https://github.com/paulmillr/chokidar).
-At first run it will check and install it if required.
-
## Usage
All the examples assume you've installed the package globally. If you opted for the local installation prepend `vendor/bin/` everywhere where `php-watcher` is mentioned.
@@ -236,9 +234,28 @@ script crashes PHP-watcher will notify you about that.

+## Performance
+
+The watcher can use different strategies to monitor your file system changes. Under the hood it
+detects the environment and chooses the best suitable strategy.
+
+### Fswatch (OSX only)
+
+[FsWatch](https://github.com/emcrisostomo/fswatch) is a cross-platform (Linux,Mac,Windows) file change monitor that will automatically
+use the platforms native functionality when possible. Under the hood the filesystem notifies us
+when any changes occur. Currently, it [doesn't work correctly on Linux](https://github.com/emcrisostomo/fswatch/issues/247).
+If your system is OSx and has fswatch installed, this strategy will be used.
+
+**Has not been extensively tested.**
+
+### Chokidar
+
+[Chokidar](https://github.com/paulmillr/chokidar) is a JavaScript package for watching file and directory changes.
+At first run the watcher will check if Node.js is available in the system. If it is, it will install chokidar into the project.
+
## Spinner
-By default the watcher outputs a nice spinner which indicates that the process is running
+By default, the watcher outputs a nice spinner which indicates that the process is running
and watching your files. But if your system doesn't support ansi coded the watcher
will try to detect it and disable the spinner. Or you can always disable the spinner
manually with option '--no-spinner':
diff --git a/composer.json b/composer.json
index 4224dfc..ebb8baa 100644
--- a/composer.json
+++ b/composer.json
@@ -31,9 +31,10 @@
"react/child-process": "^0.6.1",
"react/event-loop": "^1.1",
"react/stream": "^1.0.0",
- "spatie/file-system-watcher": "^1.2",
+ "seregazhuk/reactphp-fswatch": "^1.1.1",
"symfony/console": "^6.0",
"symfony/finder": "^6.0",
+ "symfony/process": "^6.0",
"symfony/yaml": "^6.0"
},
"autoload": {
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 4c99dc1..36a7853 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -6,19 +6,16 @@
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
- stopOnFailure="true"
+ stopOnFailure="false"
bootstrap="vendor/autoload.php">
- tests
+ ./tests
src
-
- src/Filesystem/watcher.php
-
diff --git a/server.php b/server.php
deleted file mode 100644
index f83511d..0000000
--- a/server.php
+++ /dev/null
@@ -1,5 +0,0 @@
-find('node'),
- realpath(__DIR__.'/../../bin/file-watcher.js'),
- json_encode($watchList->getPaths()),
- json_encode($watchList->getIgnored()),
- json_encode($watchList->getFileExtensions()),
- ];
-
- $process = new Process(command: $command);
- $process->start();
-
- $this->loop->addPeriodicTimer(
- self::INTERVAL,
- function () use ($process): void {
- $output = $process->getIncrementalOutput();
- if ($output !== '') {
- $this->emit('change');
- }
- }
- );
- }
-
- public function onChange(callable $callback): void
- {
- $this->on('change', $callback);
- }
-}
diff --git a/src/Filesystem/ChangesListener/ChangesListenerInterface.php b/src/Filesystem/ChangesListener/ChangesListenerInterface.php
new file mode 100644
index 0000000..815328c
--- /dev/null
+++ b/src/Filesystem/ChangesListener/ChangesListenerInterface.php
@@ -0,0 +1,16 @@
+find('node'),
+ realpath(__DIR__.'/../../../bin/file-watcher.js'),
+ json_encode($watchList->getPaths()),
+ json_encode($watchList->getIgnored()),
+ json_encode($watchList->getFileExtensions()),
+ ];
+
+ $this->process = new Process(command: $command);
+ $this->process->start();
+
+ $this->timer = $this->loop->addPeriodicTimer(
+ self::INTERVAL,
+ function (): void {
+ $output = $this->process->getIncrementalOutput();
+ if ($output !== '') {
+ $this->emit('change');
+ }
+ }
+ );
+ }
+
+ public function onChange(callable $callback): void
+ {
+ $this->on('change', $callback);
+ }
+
+ public function stop(): void
+ {
+ if ($this->process instanceof Process && $this->process->isRunning()) {
+ $this->process->stop();
+ }
+
+ if ($this->timer instanceof TimerInterface) {
+ $this->loop->cancelTimer($this->timer);
+ }
+ }
+
+ public function getName(): string
+ {
+ return 'chokidar';
+ }
+}
diff --git a/src/Filesystem/ChangesListener/FSWatchChangesListener.php b/src/Filesystem/ChangesListener/FSWatchChangesListener.php
new file mode 100644
index 0000000..2435b00
--- /dev/null
+++ b/src/Filesystem/ChangesListener/FSWatchChangesListener.php
@@ -0,0 +1,129 @@
+getIgnored() as $ignoredPath) {
+ if (realpath($ignoredPath) === false && basename($path) === $ignoredPath) {
+ return true;
+ }
+ if (realpath($ignoredPath) !== false && $path === $ignoredPath) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ $argsAndOptions = $this->makeOptions($watchList);
+ $this->process = new Process(command: ['fswatch', '-xrn', ...$argsAndOptions]);
+ $this->process->start();
+
+ $this->timer = $this->loop->addPeriodicTimer(
+ self::INTERVAL,
+ function () use ($checkPathIsIgnored): void {
+ $output = $this->process->getIncrementalOutput();
+ if ($output === '') {
+ return;
+ }
+ $lines = explode("\n", $output);
+ foreach ($lines as $line) {
+ if ($line === '') {
+ continue;
+ }
+ [$path] = explode(' ', $line);
+ if (! $checkPathIsIgnored($path)) {
+ $this->emit('change');
+ }
+ }
+ }
+ );
+ }
+
+ public function onChange(callable $callback): void
+ {
+ $this->on('change', $callback);
+ }
+
+ public function stop(): void
+ {
+ if ($this->process instanceof Process && $this->process->isRunning()) {
+ $this->process->stop();
+ }
+
+ if ($this->timer instanceof TimerInterface) {
+ $this->loop->cancelTimer($this->timer);
+ }
+ }
+
+ /**
+ * @return string[]
+ */
+ private function makeOptions(WatchList $watchList): array
+ {
+ $options = [];
+
+ // first come paths
+ if ($watchList->getPaths() !== []) {
+ $options[] = implode(' ', $watchList->getPaths());
+ }
+
+ // then include
+ if ($watchList->getFileExtensions() !== []) {
+ $options = array_merge($options, $this->makeIncludeOptions($watchList));
+ }
+
+ return $options;
+ }
+
+ /**
+ * @return string[]
+ */
+ private function makeIncludeOptions(WatchList $watchList): array
+ {
+ // Before including we need to ignore everything
+ $options[] = '-e';
+ $options[] = '.*';
+ $options[] = '-i';
+
+ $regexpWithExtensions = array_map(
+ static fn ($extension): string => str_replace(['*.', '.'], '\\.', $extension).'$',
+ $watchList->getFileExtensions()
+ );
+
+ return array_merge($options, $regexpWithExtensions);
+ }
+
+ public function getName(): string
+ {
+ return 'fswatch';
+ }
+}
diff --git a/src/Filesystem/FilesystemChangesListenerFactory.php b/src/Filesystem/FilesystemChangesListenerFactory.php
new file mode 100644
index 0000000..9185f5f
--- /dev/null
+++ b/src/Filesystem/FilesystemChangesListenerFactory.php
@@ -0,0 +1,35 @@
+comment('Chokidar is not installed in the project.');
+ $screen->comment('Installing chokidar...');
+ SystemRequirementsChecker::installChokidar();
+ }
+
+ return new ChokidarChangesListener($loop);
+ }
+
+ throw new SystemRequirementsNotMetException('Neither Node.js nor fswatch are installed.');
+ }
+}
diff --git a/src/Filesystem/WatchPath.php b/src/Filesystem/WatchPath.php
deleted file mode 100644
index ca380f3..0000000
--- a/src/Filesystem/WatchPath.php
+++ /dev/null
@@ -1,39 +0,0 @@
-isDirectory()) {
- return true;
- }
-
- return ! file_exists($this->pattern);
- }
-
- private function directoryPart(): string
- {
- return pathinfo($this->pattern, PATHINFO_DIRNAME);
- }
-
- public function fileName(): string
- {
- return pathinfo($this->pattern, PATHINFO_BASENAME);
- }
-
- private function isDirectory(): bool
- {
- return is_dir($this->pattern);
- }
-
- public function path(): string
- {
- return $this->isDirectory() ? $this->pattern : $this->directoryPart();
- }
-}
diff --git a/src/ProcessRunner.php b/src/ProcessRunner.php
index d2fac37..d5f2c57 100644
--- a/src/ProcessRunner.php
+++ b/src/ProcessRunner.php
@@ -23,7 +23,9 @@ public function start(): void
$this->screen->start($this->process->getCommand());
$this->screen->showSpinner($this->loop);
- $this->process->start($this->loop);
+ if (! $this->process->isRunning()) {
+ $this->process->start();
+ }
$this->subscribeToProcessOutput();
}
@@ -35,7 +37,7 @@ public function stop(int $signal): void
public function restart(float $delayToRestart): void
{
- $this->screen->restarting($this->process->getCommand());
+ $this->screen->restarting();
$this->loop->addTimer($delayToRestart, $this->start(...));
}
diff --git a/src/Screen/Screen.php b/src/Screen/Screen.php
index 050762e..c6b83a6 100644
--- a/src/Screen/Screen.php
+++ b/src/Screen/Screen.php
@@ -6,12 +6,16 @@
use AlecRabbit\Snake\Contracts\SpinnerInterface;
use React\EventLoop\LoopInterface;
+use React\EventLoop\TimerInterface;
use seregazhuk\PhpWatcher\Config\WatchList;
use seregazhuk\PhpWatcher\ConsoleApplication;
+use seregazhuk\PhpWatcher\Filesystem\ChangesListener\ChangesListenerInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
final class Screen
{
+ private ?TimerInterface $spinTimer = null;
+
public function __construct(private readonly SymfonyStyle $output, private readonly SpinnerInterface $spinner) {}
public function showOptions(WatchList $watchList): void
@@ -59,15 +63,20 @@ public function start(string $command): void
$this->info(sprintf('starting `%s`', trim($command)));
}
- public function restarting(?string $command = null): void
+ public function restarting(): void
{
$this->spinner->erase();
$this->output->writeln('');
$this->info('restarting due to changes...');
}
- public function processExit(int $exitCode): void
+ public function processExit(?int $exitCode): void
{
+ if ($exitCode === null) {
+ $this->info('Stopping watcher...');
+
+ return;
+ }
if ($exitCode === 0) {
$this->info('clean exit - waiting for changes before restart');
} else {
@@ -83,13 +92,26 @@ public function plainOutput(string $data): void
public function showSpinner(LoopInterface $loop): void
{
$this->spinner->begin();
- $loop->addPeriodicTimer($this->spinner->interval(), function (): void {
+ $this->spinTimer = $loop->addPeriodicTimer($this->spinner->interval(), function (): void {
$this->spinner->spin();
});
}
+ public function stop(LoopInterface $loop): void
+ {
+ $this->spinner->end();
+ if ($this->spinTimer instanceof TimerInterface) {
+ $loop->cancelTimer($this->spinTimer);
+ }
+ }
+
private function message(string $text): string
{
return sprintf('[%s] %s', ConsoleApplication::NAME, $text);
}
+
+ public function showFilesystemListener(ChangesListenerInterface $filesystemListener): void
+ {
+ $this->comment('using filesystem listener: '.$filesystemListener->getName());
+ }
}
diff --git a/src/SystemRequirements/SystemRequirements.php b/src/SystemRequirements/SystemRequirements.php
deleted file mode 100644
index c27086f..0000000
--- a/src/SystemRequirements/SystemRequirements.php
+++ /dev/null
@@ -1,56 +0,0 @@
-isNodeJsInstalled() === false) {
- $this->screen->warning('Node.js is not installed.');
- $this->screen->warning('Please install it from https://nodejs.org/en/download/');
-
- return false;
- }
-
- if ($this->isChokidarInstalled() === false) {
- $this->screen->comment('Chokidar is not installed in the project.');
- $this->screen->comment('Installing chokidar...');
- $this->installChokidar();
- }
-
- return true;
- }
-
- public function isNodeJsInstalled(): bool
- {
- $process = new Process(command: ['node', '-v']);
- $process->start();
- $process->wait();
-
- return $process->getExitCode() === 0;
- }
-
- public function isChokidarInstalled(): bool
- {
- $process = new Process(command: ['npm', 'list', 'chokidar']);
- $process->start();
- $process->wait();
-
- return $process->getExitCode() === 0 && str_contains($process->getOutput(), 'chokidar');
- }
-
- public function installChokidar(): void
- {
- $process = new Process(command: ['npm', 'install', 'chokidar']);
- $process->start();
- $process->wait();
- }
-}
diff --git a/src/SystemRequirements/SystemRequirementsChecker.php b/src/SystemRequirements/SystemRequirementsChecker.php
new file mode 100644
index 0000000..c219b49
--- /dev/null
+++ b/src/SystemRequirements/SystemRequirementsChecker.php
@@ -0,0 +1,47 @@
+start();
+ $process->wait();
+
+ return $process->isSuccessful() && PHP_OS_FAMILY === 'Darwin';
+ }
+
+ public static function isNodeJsInstalled(): bool
+ {
+ $process = new Process(command: ['node', '-v']);
+ $process->start();
+ $process->wait();
+
+ return $process->isSuccessful();
+ }
+
+ public static function isChokidarInstalled(): bool
+ {
+ $process = new Process(command: ['npm', 'list', 'chokidar']);
+ $process->start();
+ $process->wait();
+
+ return $process->isSuccessful() && str_contains($process->getOutput(), 'chokidar');
+ }
+
+ public static function installChokidar(): void
+ {
+ $process = new Process(command: ['npm', 'install', 'chokidar']);
+ $process->start();
+ $process->wait();
+ }
+}
diff --git a/src/SystemRequirements/SystemRequirementsNotMetException.php b/src/SystemRequirements/SystemRequirementsNotMetException.php
new file mode 100644
index 0000000..19ab0c1
--- /dev/null
+++ b/src/SystemRequirements/SystemRequirementsNotMetException.php
@@ -0,0 +1,7 @@
+spinnerDisabled());
$screen = new Screen(new SymfonyStyle($input, $output), $spinner);
- $requirements = new SystemRequirements($screen);
- if ($requirements->check() === false) {
+ $loop = Loop::get();
+ try {
+ $filesystemListener = FilesystemChangesListenerFactory::create($loop, $screen);
+ } catch (SystemRequirementsNotMetException $e) {
+ $screen->warning($e->getMessage());
+
return Command::FAILURE;
}
+ $this->addTerminationListeners($loop, $screen, $filesystemListener);
- $loop = Loop::get();
- $this->addTerminationListeners($loop, $spinner);
- $filesystem = new ChangesListener($loop);
$screen->showOptions($config->watchList());
+ $screen->showFilesystemListener($filesystemListener);
$processRunner = new ProcessRunner($loop, $screen, $config->command());
- $watcher = new Watcher($loop, $filesystem);
+ $watcher = new Watcher($loop, $filesystemListener);
$watcher->startWatching(
$processRunner,
$config->watchList(),
@@ -88,10 +91,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
/**
* When terminating the watcher, we need to manually restore the cursor after the spinner.
*/
- private function addTerminationListeners(LoopInterface $loop, SpinnerInterface $spinner): void
+ private function addTerminationListeners(LoopInterface $loop, Screen $screen, ChangesListenerInterface $changesListener): void
{
- $func = static function (int $signal) use ($spinner): never {
- $spinner->end();
+ $func = static function (int $signal) use ($screen, $changesListener, $loop): never {
+ $screen->stop($loop);
+ $changesListener->stop();
+ $loop->stop();
exit($signal);
};
diff --git a/tests/Feature/ChokidarChangesListenerTest.php b/tests/Feature/ChokidarChangesListenerTest.php
new file mode 100644
index 0000000..0d0fcc1
--- /dev/null
+++ b/tests/Feature/ChokidarChangesListenerTest.php
@@ -0,0 +1,42 @@
+markTestSkipped('chokidar is not available');
+ }
+
+ $loop = Loop::get();
+ $listener = new ChokidarChangesListener($loop);
+ $listener->start(new WatchList([Filesystem::fixturesDir()]));
+
+ $loop->addTimer(1, Filesystem::createHelloWorldPHPFile(...));
+ $eventWasEmitted = false;
+ $listener->onChange(function () use (&$eventWasEmitted): void {
+ $eventWasEmitted = true;
+ });
+ delay(4);
+ $this->assertTrue($eventWasEmitted, '"change" event should be emitted');
+ $listener->stop();
+ }
+}
diff --git a/tests/Feature/ChangesListenerTest.php b/tests/Feature/FsWatchChangesListenerTest.php
similarity index 61%
rename from tests/Feature/ChangesListenerTest.php
rename to tests/Feature/FsWatchChangesListenerTest.php
index 7843f4d..00f7247 100644
--- a/tests/Feature/ChangesListenerTest.php
+++ b/tests/Feature/FsWatchChangesListenerTest.php
@@ -2,41 +2,42 @@
declare(strict_types=1);
-namespace seregazhuk\PhpWatcher\Tests\Feature;
+namespace Feature;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use React\EventLoop\Loop;
use seregazhuk\PhpWatcher\Config\WatchList;
-use seregazhuk\PhpWatcher\Filesystem\ChangesListener;
+use seregazhuk\PhpWatcher\Filesystem\ChangesListener\FSWatchChangesListener;
use seregazhuk\PhpWatcher\Tests\Feature\Helper\Filesystem;
use seregazhuk\PhpWatcher\Tests\Feature\Helper\WithFilesystem;
use function React\Async\delay;
-final class ChangesListenerTest extends TestCase
+final class FsWatchChangesListenerTest extends TestCase
{
use WithFilesystem;
- protected function tearDown(): void
- {
- Loop::get()->stop();
- parent::tearDown();
- }
-
#[Test]
public function it_emits_change_event_on_changes(): void
{
+ if (! FSWatchChangesListener::isAvailable()) {
+ $this->markTestSkipped('fswatch is not available');
+ }
+
$loop = Loop::get();
- $listener = new ChangesListener($loop);
- $listener->start(new WatchList([Filesystem::fixturesDir()]));
+ $listener = new FSWatchChangesListener($loop);
- $loop->addTimer(1, Filesystem::createHelloWorldPHPFile(...));
$eventWasEmitted = false;
- $listener->on('change', static function () use (&$eventWasEmitted): void {
+ $listener->onChange(function () use (&$eventWasEmitted): void {
$eventWasEmitted = true;
});
- delay(4); // to be sure changes have been detected
+ $loop->addTimer(1, Filesystem::createHelloWorldPHPFile(...));
+ $loop->addTimer(3, fn () => $loop->stop());
+
+ $listener->start(new WatchList([Filesystem::fixturesDir()]));
+ delay(2);
+
$this->assertTrue($eventWasEmitted, '"change" event should be emitted');
}
}
diff --git a/tests/Feature/Helper/Filesystem.php b/tests/Feature/Helper/Filesystem.php
index d867f5f..56e20ff 100644
--- a/tests/Feature/Helper/Filesystem.php
+++ b/tests/Feature/Helper/Filesystem.php
@@ -49,7 +49,7 @@ public static function createHelloWorldPHPFileWithSignalsHandling(): string
pcntl_signal(SIGINT, "handler");
while (true) {
- echo "Hello, world";
+ echo "Hello, world\n";
sleep(1);
}
function handler($signal) {
diff --git a/tests/Feature/Helper/WatcherRunner.php b/tests/Feature/Helper/WatcherRunner.php
index 1f936a5..960993e 100644
--- a/tests/Feature/Helper/WatcherRunner.php
+++ b/tests/Feature/Helper/WatcherRunner.php
@@ -13,7 +13,7 @@ final class WatcherRunner
*/
public function run(string $scriptToRun, array $arguments = []): Process
{
- $arguments = array_merge($arguments, ['--delay', 0.25]);
+ $arguments = array_merge($arguments, ['--delay', 1]);
$process = new Process(array_merge(['./php-watcher', $scriptToRun], $arguments));
$process->start();
diff --git a/tests/Feature/IgnoreFilesTest.php b/tests/Feature/IgnoreFilesTest.php
index 832deca..83bb555 100644
--- a/tests/Feature/IgnoreFilesTest.php
+++ b/tests/Feature/IgnoreFilesTest.php
@@ -19,7 +19,7 @@ public function it_doesnt_reload_when_ignored_files_change(): void
Filesystem::changeFileContentsWith($fileToWatch, 'wait();
- $this->assertOutputDoesntContain('restarting due to changes...');
+ $this->assertOutputDoesntContain('restarting due to changes');
}
#[Test]
@@ -31,6 +31,6 @@ public function it_doesnt_reload_when_ignored_directories_change(): void
Filesystem::changeFileContentsWith($fileToWatch, 'wait();
- $this->assertOutputDoesntContain('restarting due to changes...');
+ $this->assertOutputDoesntContain('restarting due to changes');
}
}
diff --git a/tests/Feature/SignalTest.php b/tests/Feature/SignalTest.php
index 31f7692..a7aefec 100644
--- a/tests/Feature/SignalTest.php
+++ b/tests/Feature/SignalTest.php
@@ -13,7 +13,7 @@ final class SignalTest extends WatcherTestCase
#[Test]
public function it_sends_a_specified_signal_to_restart_the_app(): void
{
- if (! defined('SIGTERM')) {
+ if (! defined('SIGTERM') || ! extension_loaded('pcntl')) {
$this->markTestSkipped('SIGTERM is not defined');
}
diff --git a/tests/Unit/WatchPathTest.php b/tests/Unit/WatchPathTest.php
deleted file mode 100644
index 4d3585b..0000000
--- a/tests/Unit/WatchPathTest.php
+++ /dev/null
@@ -1,55 +0,0 @@
-assertEquals('/root/test', $path->path());
- }
-
- /** @test */
- public function it_provides_a_directory_path_for_a_directory(): void
- {
- $path = new WatchPath('/root/test');
- $this->assertEquals('/root', $path->path());
- }
-
- /** @test */
- public function it_provides_a_filename_for_a_file(): void
- {
- $path = new WatchPath('/root/test.txt');
- $this->assertEquals('test.txt', $path->fileName());
- }
-
- /** @test */
- public function it_provides_a_pattern_path_for_a_pattern(): void
- {
- $path = new WatchPath('/root/test.*');
- $this->assertEquals('test.*', $path->fileName());
- }
-
- /** @test */
- public function it_can_detect_pattern_or_a_file(): void
- {
- $path = new WatchPath('/root/test.*');
- $this->assertTrue($path->isFileOrPattern());
-
- $path = new WatchPath('/root/test.txt');
- $this->assertTrue($path->isFileOrPattern());
-
- $path = new WatchPath('/root/*.txt');
- $this->assertTrue($path->isFileOrPattern());
-
- $path = new WatchPath(__DIR__);
- $this->assertFalse($path->isFileOrPattern());
- }
-}