-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request dart-lang/watcher#91 from michalt/custom-watcher-f…
…actory Implement the ability to register custom watcher implementations
- Loading branch information
Showing
5 changed files
with
246 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import '../watcher.dart'; | ||
|
||
/// Defines a way to create a custom watcher instead of the default ones. | ||
/// | ||
/// This will be used when a [DirectoryWatcher] or [FileWatcher] would be | ||
/// created and will take precedence over the default ones. | ||
abstract class CustomWatcherFactory { | ||
/// Uniquely identify this watcher. | ||
String get id; | ||
|
||
/// Tries to create a [DirectoryWatcher] for the provided path. | ||
/// | ||
/// Returns `null` if the path is not supported by this factory. | ||
DirectoryWatcher createDirectoryWatcher(String path, {Duration pollingDelay}); | ||
|
||
/// Tries to create a [FileWatcher] for the provided path. | ||
/// | ||
/// Returns `null` if the path is not supported by this factory. | ||
FileWatcher createFileWatcher(String path, {Duration pollingDelay}); | ||
} | ||
|
||
/// Registers a custom watcher. | ||
/// | ||
/// It's only allowed to register a watcher factory once per [id] and at most | ||
/// one factory should apply to any given file (creating a [Watcher] will fail | ||
/// otherwise). | ||
void registerCustomWatcherFactory(CustomWatcherFactory customFactory) { | ||
if (_customWatcherFactories.containsKey(customFactory.id)) { | ||
throw ArgumentError('A custom watcher with id `${customFactory.id}` ' | ||
'has already been registered'); | ||
} | ||
_customWatcherFactories[customFactory.id] = customFactory; | ||
} | ||
|
||
/// Tries to create a custom [DirectoryWatcher] and returns it. | ||
/// | ||
/// Returns `null` if no custom watcher was applicable and throws a [StateError] | ||
/// if more than one was. | ||
DirectoryWatcher createCustomDirectoryWatcher(String path, | ||
{Duration pollingDelay}) { | ||
DirectoryWatcher customWatcher; | ||
String customFactoryId; | ||
for (var watcherFactory in customWatcherFactories) { | ||
if (customWatcher != null) { | ||
throw StateError('Two `CustomWatcherFactory`s applicable: ' | ||
'`$customFactoryId` and `${watcherFactory.id}` for `$path`'); | ||
} | ||
customWatcher = | ||
watcherFactory.createDirectoryWatcher(path, pollingDelay: pollingDelay); | ||
customFactoryId = watcherFactory.id; | ||
} | ||
return customWatcher; | ||
} | ||
|
||
/// Tries to create a custom [FileWatcher] and returns it. | ||
/// | ||
/// Returns `null` if no custom watcher was applicable and throws a [StateError] | ||
/// if more than one was. | ||
FileWatcher createCustomFileWatcher(String path, {Duration pollingDelay}) { | ||
FileWatcher customWatcher; | ||
String customFactoryId; | ||
for (var watcherFactory in customWatcherFactories) { | ||
if (customWatcher != null) { | ||
throw StateError('Two `CustomWatcherFactory`s applicable: ' | ||
'`$customFactoryId` and `${watcherFactory.id}` for `$path`'); | ||
} | ||
customWatcher = | ||
watcherFactory.createFileWatcher(path, pollingDelay: pollingDelay); | ||
customFactoryId = watcherFactory.id; | ||
} | ||
return customWatcher; | ||
} | ||
|
||
/// Unregisters a custom watcher and returns it. | ||
/// | ||
/// Returns `null` if the id was never registered. | ||
CustomWatcherFactory unregisterCustomWatcherFactory(String id) => | ||
_customWatcherFactories.remove(id); | ||
|
||
Iterable<CustomWatcherFactory> get customWatcherFactories => | ||
_customWatcherFactories.values; | ||
|
||
final _customWatcherFactories = <String, CustomWatcherFactory>{}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import 'dart:async'; | ||
|
||
import 'package:test/test.dart'; | ||
import 'package:watcher/watcher.dart'; | ||
|
||
import 'utils.dart'; | ||
|
||
void main() { | ||
_MemFs memFs; | ||
final defaultFactoryId = 'MemFs'; | ||
|
||
setUp(() { | ||
memFs = _MemFs(); | ||
registerCustomWatcherFactory(_MemFsWatcherFactory(defaultFactoryId, memFs)); | ||
}); | ||
|
||
tearDown(() async { | ||
unregisterCustomWatcherFactory(defaultFactoryId); | ||
}); | ||
|
||
test('notifes for files', () async { | ||
var watcher = FileWatcher('file.txt'); | ||
|
||
var completer = Completer<WatchEvent>(); | ||
watcher.events.listen((event) => completer.complete(event)); | ||
await watcher.ready; | ||
memFs.add('file.txt'); | ||
var event = await completer.future; | ||
|
||
expect(event.type, ChangeType.ADD); | ||
expect(event.path, 'file.txt'); | ||
}); | ||
|
||
test('notifes for directories', () async { | ||
var watcher = DirectoryWatcher('dir'); | ||
|
||
var completer = Completer<WatchEvent>(); | ||
watcher.events.listen((event) => completer.complete(event)); | ||
await watcher.ready; | ||
memFs.add('dir'); | ||
var event = await completer.future; | ||
|
||
expect(event.type, ChangeType.ADD); | ||
expect(event.path, 'dir'); | ||
}); | ||
|
||
test('unregister works', () async { | ||
unregisterCustomWatcherFactory(defaultFactoryId); | ||
|
||
watcherFactory = (path) => FileWatcher(path); | ||
try { | ||
// This uses standard files, so it wouldn't trigger an event in | ||
// _MemFsWatcher. | ||
writeFile('file.txt'); | ||
await startWatcher(path: 'file.txt'); | ||
deleteFile('file.txt'); | ||
} finally { | ||
watcherFactory = null; | ||
} | ||
|
||
await expectRemoveEvent('file.txt'); | ||
}); | ||
|
||
test('registering twice throws', () async { | ||
expect( | ||
() => registerCustomWatcherFactory( | ||
_MemFsWatcherFactory(defaultFactoryId, memFs)), | ||
throwsA(isA<ArgumentError>())); | ||
}); | ||
|
||
test('finding two applicable factories throws', () async { | ||
// Note that _MemFsWatcherFactory always returns a watcher, so having two | ||
// will always produce a conflict. | ||
registerCustomWatcherFactory(_MemFsWatcherFactory('Different id', memFs)); | ||
expect(() => FileWatcher('file.txt'), throwsA(isA<StateError>())); | ||
expect(() => DirectoryWatcher('dir'), throwsA(isA<StateError>())); | ||
}); | ||
} | ||
|
||
class _MemFs { | ||
final _streams = <String, Set<StreamController<WatchEvent>>>{}; | ||
|
||
StreamController<WatchEvent> watchStream(String path) { | ||
var controller = StreamController<WatchEvent>(); | ||
_streams | ||
.putIfAbsent(path, () => <StreamController<WatchEvent>>{}) | ||
.add(controller); | ||
return controller; | ||
} | ||
|
||
void add(String path) { | ||
var controllers = _streams[path]; | ||
if (controllers != null) { | ||
for (var controller in controllers) { | ||
controller.add(WatchEvent(ChangeType.ADD, path)); | ||
} | ||
} | ||
} | ||
|
||
void remove(String path) { | ||
var controllers = _streams[path]; | ||
if (controllers != null) { | ||
for (var controller in controllers) { | ||
controller.add(WatchEvent(ChangeType.REMOVE, path)); | ||
} | ||
} | ||
} | ||
} | ||
|
||
class _MemFsWatcher implements FileWatcher, DirectoryWatcher, Watcher { | ||
final String _path; | ||
final StreamController<WatchEvent> _controller; | ||
|
||
_MemFsWatcher(this._path, this._controller); | ||
|
||
@override | ||
String get path => _path; | ||
|
||
@override | ||
String get directory => throw UnsupportedError('directory is not supported'); | ||
|
||
@override | ||
Stream<WatchEvent> get events => _controller.stream; | ||
|
||
@override | ||
bool get isReady => true; | ||
|
||
@override | ||
Future<void> get ready async {} | ||
} | ||
|
||
class _MemFsWatcherFactory implements CustomWatcherFactory { | ||
@override | ||
final String id; | ||
final _MemFs _memFs; | ||
_MemFsWatcherFactory(this.id, this._memFs); | ||
|
||
@override | ||
DirectoryWatcher createDirectoryWatcher(String path, | ||
{Duration pollingDelay}) => | ||
_MemFsWatcher(path, _memFs.watchStream(path)); | ||
|
||
@override | ||
FileWatcher createFileWatcher(String path, {Duration pollingDelay}) => | ||
_MemFsWatcher(path, _memFs.watchStream(path)); | ||
} |