158 lines
5.3 KiB
TypeScript
158 lines
5.3 KiB
TypeScript
|
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||
|
|
import { RustWatcher } from '../ts/watchers/watcher.rust.js';
|
||
|
|
import type { IWatchEvent } from '../ts/watchers/interfaces.js';
|
||
|
|
import * as path from 'path';
|
||
|
|
import * as fs from 'fs';
|
||
|
|
|
||
|
|
const TEST_DIR = path.resolve('./test/assets');
|
||
|
|
const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
||
|
|
|
||
|
|
function waitForEvent(
|
||
|
|
watcher: { events$: { subscribe: Function } },
|
||
|
|
filter: (e: IWatchEvent) => boolean,
|
||
|
|
timeoutMs = 5000
|
||
|
|
): Promise<IWatchEvent> {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const timeout = setTimeout(() => {
|
||
|
|
sub.unsubscribe();
|
||
|
|
reject(new Error(`Timeout waiting for event after ${timeoutMs}ms`));
|
||
|
|
}, timeoutMs);
|
||
|
|
const sub = watcher.events$.subscribe((event: IWatchEvent) => {
|
||
|
|
if (filter(event)) {
|
||
|
|
clearTimeout(timeout);
|
||
|
|
sub.unsubscribe();
|
||
|
|
resolve(event);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function collectEvents(
|
||
|
|
watcher: { events$: { subscribe: Function } },
|
||
|
|
filter: (e: IWatchEvent) => boolean,
|
||
|
|
durationMs: number
|
||
|
|
): Promise<IWatchEvent[]> {
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
const events: IWatchEvent[] = [];
|
||
|
|
const sub = watcher.events$.subscribe((event: IWatchEvent) => {
|
||
|
|
if (filter(event)) events.push(event);
|
||
|
|
});
|
||
|
|
setTimeout(() => { sub.unsubscribe(); resolve(events); }, durationMs);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
let available = false;
|
||
|
|
|
||
|
|
tap.test('RustWatcher (Node): check availability', async () => {
|
||
|
|
available = await RustWatcher.isAvailable();
|
||
|
|
console.log(`[test] Rust binary available: ${available}`);
|
||
|
|
if (!available) {
|
||
|
|
console.log('[test] Skipping Rust watcher tests — binary not found');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
let watcher: RustWatcher;
|
||
|
|
|
||
|
|
tap.test('RustWatcher (Node): should create and start', async () => {
|
||
|
|
if (!available) return;
|
||
|
|
watcher = new RustWatcher({
|
||
|
|
basePaths: [TEST_DIR],
|
||
|
|
depth: 4,
|
||
|
|
followSymlinks: false,
|
||
|
|
debounceMs: 100,
|
||
|
|
});
|
||
|
|
expect(watcher.isWatching).toBeFalse();
|
||
|
|
await watcher.start();
|
||
|
|
expect(watcher.isWatching).toBeTrue();
|
||
|
|
await delay(300);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('RustWatcher (Node): should emit initial add events', async () => {
|
||
|
|
if (!available) return;
|
||
|
|
// The initial scan should have emitted add events for existing files.
|
||
|
|
// We verify by creating a file and checking it gets an add event
|
||
|
|
const file = path.join(TEST_DIR, 'rust-node-add.txt');
|
||
|
|
const eventPromise = waitForEvent(watcher, (e) => e.type === 'add' && e.path.includes('rust-node-add.txt'));
|
||
|
|
await fs.promises.writeFile(file, 'rust node add test');
|
||
|
|
const event = await eventPromise;
|
||
|
|
expect(event.type).toEqual('add');
|
||
|
|
expect(event.path).toInclude('rust-node-add.txt');
|
||
|
|
await fs.promises.unlink(file);
|
||
|
|
await delay(200);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('RustWatcher (Node): should detect file modification', async () => {
|
||
|
|
if (!available) return;
|
||
|
|
const file = path.join(TEST_DIR, 'rust-node-change.txt');
|
||
|
|
await fs.promises.writeFile(file, 'initial');
|
||
|
|
await delay(300);
|
||
|
|
|
||
|
|
const eventPromise = waitForEvent(watcher, (e) => e.type === 'change' && e.path.includes('rust-node-change.txt'));
|
||
|
|
await fs.promises.writeFile(file, 'modified');
|
||
|
|
const event = await eventPromise;
|
||
|
|
expect(event.type).toEqual('change');
|
||
|
|
|
||
|
|
await fs.promises.unlink(file);
|
||
|
|
await delay(200);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('RustWatcher (Node): should detect file deletion', async () => {
|
||
|
|
if (!available) return;
|
||
|
|
const file = path.join(TEST_DIR, 'rust-node-unlink.txt');
|
||
|
|
await fs.promises.writeFile(file, 'to delete');
|
||
|
|
await delay(300);
|
||
|
|
|
||
|
|
const eventPromise = waitForEvent(watcher, (e) => e.type === 'unlink' && e.path.includes('rust-node-unlink.txt'));
|
||
|
|
await fs.promises.unlink(file);
|
||
|
|
const event = await eventPromise;
|
||
|
|
expect(event.type).toEqual('unlink');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('RustWatcher (Node): should detect directory creation', async () => {
|
||
|
|
if (!available) return;
|
||
|
|
const dir = path.join(TEST_DIR, 'rust-node-subdir');
|
||
|
|
const eventPromise = waitForEvent(watcher, (e) => e.type === 'addDir' && e.path.includes('rust-node-subdir'));
|
||
|
|
await fs.promises.mkdir(dir, { recursive: true });
|
||
|
|
const event = await eventPromise;
|
||
|
|
expect(event.type).toEqual('addDir');
|
||
|
|
await delay(200);
|
||
|
|
await fs.promises.rmdir(dir);
|
||
|
|
await delay(200);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('RustWatcher (Node): should handle rapid modifications', async () => {
|
||
|
|
if (!available) return;
|
||
|
|
const file = path.join(TEST_DIR, 'rust-node-rapid.txt');
|
||
|
|
await fs.promises.writeFile(file, 'initial');
|
||
|
|
await delay(200);
|
||
|
|
|
||
|
|
const collector = collectEvents(watcher, (e) => e.type === 'change' && e.path.includes('rust-node-rapid.txt'), 3000);
|
||
|
|
|
||
|
|
for (let i = 0; i < 10; i++) {
|
||
|
|
await fs.promises.writeFile(file, `content ${i}`);
|
||
|
|
await delay(10);
|
||
|
|
}
|
||
|
|
|
||
|
|
const events = await collector;
|
||
|
|
console.log(`[test] Rapid mods: 10 writes, ${events.length} events received`);
|
||
|
|
expect(events.length).toBeGreaterThan(0);
|
||
|
|
|
||
|
|
await fs.promises.unlink(file);
|
||
|
|
await delay(200);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('RustWatcher (Node): should not be watching after stop', async () => {
|
||
|
|
if (!available) return;
|
||
|
|
await watcher.stop();
|
||
|
|
expect(watcher.isWatching).toBeFalse();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('RustWatcher (Node): cleanup', async () => {
|
||
|
|
for (const name of ['rust-node-add.txt', 'rust-node-change.txt', 'rust-node-unlink.txt', 'rust-node-rapid.txt']) {
|
||
|
|
try { await fs.promises.unlink(path.join(TEST_DIR, name)); } catch {}
|
||
|
|
}
|
||
|
|
try { await fs.promises.rmdir(path.join(TEST_DIR, 'rust-node-subdir')); } catch {}
|
||
|
|
});
|
||
|
|
|
||
|
|
export default tap.start();
|