Skip to content

Commit e0f8787

Browse files
committed
Add test for watcher
1 parent 59bfada commit e0f8787

File tree

3 files changed

+281
-0
lines changed

3 files changed

+281
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
},
2222
"autoload-dev": {
2323
"psr-4": {
24+
"phpDocumentor\\DevServer\\": ["packages/dev-server/tests/unit/"],
2425
"phpDocumentor\\Guides\\": ["packages/guides/tests/unit/", "tests/"],
2526
"phpDocumentor\\Guides\\Cli\\": "packages/guides-cli/tests/unit",
2627
"phpDocumentor\\Guides\\Code\\": "packages/guides-code/tests/unit",

packages/dev-server/src/Watcher/INotifyWatcher.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use const IN_CLOSE_WRITE;
2828
use const IN_CREATE;
2929
use const IN_DELETE;
30+
use const IN_IGNORED;
3031
use const IN_MODIFY;
3132

3233
class INotifyWatcher
@@ -86,6 +87,10 @@ public function __invoke(): void
8687
return;
8788
}
8889

90+
if ($event['mask'] & IN_IGNORED) {
91+
return;
92+
}
93+
8994
// Log unhandled event types for debugging
9095
var_dump('Unhandled event mask: ' . $event['mask']);
9196
}
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of phpDocumentor.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* @link https://phpdoc.org
12+
*/
13+
14+
namespace phpDocumentor\DevServer\Watcher;
15+
16+
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
17+
use PHPUnit\Framework\MockObject\MockObject;
18+
use PHPUnit\Framework\TestCase;
19+
use Psr\EventDispatcher\EventDispatcherInterface;
20+
use React\EventLoop\Loop;
21+
use React\EventLoop\LoopInterface;
22+
use RuntimeException;
23+
24+
use function fclose;
25+
use function file_put_contents;
26+
use function fopen;
27+
use function fwrite;
28+
use function glob;
29+
use function is_dir;
30+
use function is_file;
31+
use function mkdir;
32+
use function rmdir;
33+
use function sys_get_temp_dir;
34+
use function touch;
35+
use function uniqid;
36+
use function unlink;
37+
38+
class INotifyWatcherTest extends TestCase
39+
{
40+
private LoopInterface $loop;
41+
private MockObject|EventDispatcherInterface $dispatcher;
42+
private INotifyWatcher $watcher;
43+
private string $testDir;
44+
45+
/** @var FileModifiedEvent[] */
46+
private array $capturedEvents = [];
47+
48+
protected function setUp(): void
49+
{
50+
$this->loop = Loop::get();
51+
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
52+
53+
$this->testDir = sys_get_temp_dir() . '/inotify_test_' . uniqid();
54+
mkdir($this->testDir, 0755, true);
55+
56+
$this->watcher = new INotifyWatcher(
57+
$this->loop,
58+
$this->dispatcher,
59+
$this->testDir,
60+
);
61+
62+
$this->capturedEvents = [];
63+
}
64+
65+
protected function tearDown(): void
66+
{
67+
$files = glob($this->testDir . '/*');
68+
if ($files) {
69+
foreach ($files as $file) {
70+
if (!is_file($file)) {
71+
continue;
72+
}
73+
74+
unlink($file);
75+
}
76+
}
77+
78+
if (!is_dir($this->testDir)) {
79+
return;
80+
}
81+
82+
rmdir($this->testDir);
83+
}
84+
85+
#[RequiresPhpExtension('inotify')]
86+
public function testInvokeThrowsExceptionWhenInotifyIsNull(): void
87+
{
88+
$this->expectException(RuntimeException::class);
89+
$this->expectExceptionMessage('No inotify watcher');
90+
91+
($this->watcher)();
92+
}
93+
94+
#[RequiresPhpExtension('inotify')]
95+
public function testInvokeHandlesNoEvents(): void
96+
{
97+
$this->setupEventCapturing();
98+
99+
$fileName = 'test-file.txt';
100+
$filePath = $this->testDir . '/' . $fileName;
101+
102+
touch($filePath);
103+
104+
$this->watcher->addPath($fileName);
105+
106+
$this->runLoopBriefly();
107+
108+
$this->assertEmpty($this->capturedEvents);
109+
}
110+
111+
#[RequiresPhpExtension('inotify')]
112+
public function testInvokeDispatchesFileModifiedEventForInModify(): void
113+
{
114+
$this->setupEventCapturing();
115+
116+
$fileName = 'test-file.txt';
117+
$filePath = $this->testDir . '/' . $fileName;
118+
119+
touch($filePath);
120+
121+
$this->watcher->addPath($fileName);
122+
123+
file_put_contents($filePath, 'test content');
124+
125+
$this->runLoopBriefly();
126+
127+
$this->assertCount(1, $this->capturedEvents);
128+
$this->assertInstanceOf(FileModifiedEvent::class, $this->capturedEvents[0]);
129+
$this->assertEquals($fileName, $this->capturedEvents[0]->path);
130+
}
131+
132+
#[RequiresPhpExtension('inotify')]
133+
public function testInvokeDispatchesFileModifiedEventForInCloseWrite(): void
134+
{
135+
$this->setupEventCapturing();
136+
137+
$fileName = 'test-file.txt';
138+
$filePath = $this->testDir . '/' . $fileName;
139+
140+
touch($filePath);
141+
142+
$this->watcher->addPath($fileName);
143+
144+
$handle = fopen($filePath, 'w');
145+
fwrite($handle, 'test content for close write');
146+
fclose($handle);
147+
148+
$this->runLoopBriefly();
149+
150+
$this->assertCount(1, $this->capturedEvents);
151+
$this->assertInstanceOf(FileModifiedEvent::class, $this->capturedEvents[0]);
152+
$this->assertEquals($fileName, $this->capturedEvents[0]->path);
153+
}
154+
155+
#[RequiresPhpExtension('inotify')]
156+
public function testInvokeHandlesInCreateEvent(): void
157+
{
158+
$this->setupEventCapturing();
159+
160+
$fileName = 'new-file.txt';
161+
$filePath = $this->testDir . '/' . $fileName;
162+
163+
$this->watcher->addPath('.');
164+
165+
touch($filePath);
166+
167+
$this->runLoopBriefly();
168+
169+
$this->assertTrue(true); // Test passes if no exception is thrown
170+
}
171+
172+
#[RequiresPhpExtension('inotify')]
173+
public function testInvokeHandlesInDeleteEvent(): void
174+
{
175+
$this->setupEventCapturing();
176+
177+
$fileName = 'delete-file.txt';
178+
$filePath = $this->testDir . '/' . $fileName;
179+
180+
touch($filePath);
181+
182+
$this->watcher->addPath('.');
183+
184+
unlink($filePath);
185+
186+
$this->runLoopBriefly();
187+
188+
$this->assertTrue(true); // Test passes if no exception is thrown
189+
}
190+
191+
#[RequiresPhpExtension('inotify')]
192+
public function testInvokeHandlesMultipleFiles(): void
193+
{
194+
$this->setupEventCapturing();
195+
196+
$file1Name = 'file1.txt';
197+
$file2Name = 'file2.txt';
198+
$file1Path = $this->testDir . '/' . $file1Name;
199+
$file2Path = $this->testDir . '/' . $file2Name;
200+
201+
touch($file1Path);
202+
touch($file2Path);
203+
204+
$this->watcher->addPath($file1Name);
205+
$this->watcher->addPath($file2Name);
206+
207+
file_put_contents($file1Path, 'content 1');
208+
209+
$this->runLoopBriefly();
210+
211+
$this->assertCount(1, $this->capturedEvents);
212+
$this->assertEquals($file1Name, $this->capturedEvents[0]->path);
213+
214+
$this->capturedEvents = [];
215+
216+
file_put_contents($file2Path, 'content 2');
217+
218+
$this->runLoopBriefly();
219+
220+
$this->assertCount(1, $this->capturedEvents);
221+
$this->assertEquals($file2Name, $this->capturedEvents[0]->path);
222+
}
223+
224+
#[RequiresPhpExtension('inotify')]
225+
public function testAddPathInitializesInotifyOnFirstCall(): void
226+
{
227+
$fileName = 'test-file.txt';
228+
$filePath = $this->testDir . '/' . $fileName;
229+
touch($filePath);
230+
231+
$this->watcher->addPath($fileName);
232+
233+
$this->runLoopBriefly();
234+
235+
$this->assertTrue(true); // Test passes if no exception is thrown
236+
}
237+
238+
#[RequiresPhpExtension('inotify')]
239+
public function testAddPathDoesNotReinitializeInotifyOnSubsequentCalls(): void
240+
{
241+
$file1Name = 'file1.txt';
242+
$file2Name = 'file2.txt';
243+
$file1Path = $this->testDir . '/' . $file1Name;
244+
$file2Path = $this->testDir . '/' . $file2Name;
245+
246+
touch($file1Path);
247+
touch($file2Path);
248+
249+
$this->watcher->addPath($file1Name);
250+
$this->watcher->addPath($file2Name);
251+
252+
$this->runLoopBriefly();
253+
254+
$this->assertTrue(true);
255+
}
256+
257+
private function setupEventCapturing(): void
258+
{
259+
$this->dispatcher->method('dispatch')
260+
->willReturnCallback(function ($event) {
261+
$this->capturedEvents[] = $event;
262+
263+
return $event;
264+
});
265+
}
266+
267+
private function runLoopBriefly(): void
268+
{
269+
$this->loop->addTimer(0.1, function (): void {
270+
$this->loop->stop();
271+
});
272+
273+
$this->loop->run();
274+
}
275+
}

0 commit comments

Comments
 (0)