From 50dd650f93241677ee87ba93b4627cf94e0ec05d Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 14 Oct 2025 17:43:19 +0200 Subject: [PATCH 1/2] Remove backup test assertions. --- .../test/directory_watcher/file_tests.dart | 38 +---------- .../test/directory_watcher/linux_test.dart | 14 ++-- .../test/directory_watcher/mac_os_test.dart | 14 ++-- .../watcher/test/file_watcher/file_tests.dart | 4 -- pkgs/watcher/test/utils.dart | 68 +++---------------- 5 files changed, 18 insertions(+), 120 deletions(-) diff --git a/pkgs/watcher/test/directory_watcher/file_tests.dart b/pkgs/watcher/test/directory_watcher/file_tests.dart index 9b3fcd6c2..2ce9bb8aa 100644 --- a/pkgs/watcher/test/directory_watcher/file_tests.dart +++ b/pkgs/watcher/test/directory_watcher/file_tests.dart @@ -141,13 +141,6 @@ void fileTests() { }); }); - // Most of the time, when multiple filesystem actions happen in sequence, - // they'll be batched together and the watcher will see them all at once. - // These tests verify that the watcher normalizes and combine these events - // properly. However, very occasionally the events will be reported in - // separate batches, and the watcher will report them as though they occurred - // far apart in time, so each of these tests has a "backup case" to allow for - // that as well. group('clustered changes', () { test("doesn't notify when a file is created and then immediately removed", () async { @@ -155,13 +148,6 @@ void fileTests() { await startWatcher(); writeFile('file.txt'); deleteFile('file.txt'); - - // Backup case. - startClosingEventStream(); - await allowEvents(() { - expectAddEvent('file.txt'); - expectRemoveEvent('file.txt'); - }); }); test( @@ -173,13 +159,7 @@ void fileTests() { deleteFile('file.txt'); writeFile('file.txt', contents: 're-created'); - await allowEither(() { - expectModifyEvent('file.txt'); - }, () { - // Backup case. - expectRemoveEvent('file.txt'); - expectAddEvent('file.txt'); - }); + await expectModifyEvent('file.txt'); }); test( @@ -191,14 +171,7 @@ void fileTests() { renameFile('old.txt', 'new.txt'); writeFile('old.txt', contents: 're-created'); - await allowEither(() { - inAnyOrder([isModifyEvent('old.txt'), isAddEvent('new.txt')]); - }, () { - // Backup case. - expectRemoveEvent('old.txt'); - expectAddEvent('new.txt'); - expectAddEvent('old.txt'); - }); + await inAnyOrder([isModifyEvent('old.txt'), isAddEvent('new.txt')]); }); test( @@ -210,9 +183,6 @@ void fileTests() { writeFile('file.txt', contents: 'modified'); deleteFile('file.txt'); - // Backup case. - await allowModifyEvent('file.txt'); - await expectRemoveEvent('file.txt'); }); @@ -224,10 +194,6 @@ void fileTests() { writeFile('file.txt', contents: 'modified'); await expectAddEvent('file.txt'); - - // Backup case. - startClosingEventStream(); - await allowModifyEvent('file.txt'); }); }); diff --git a/pkgs/watcher/test/directory_watcher/linux_test.dart b/pkgs/watcher/test/directory_watcher/linux_test.dart index a47779466..10f6e410e 100644 --- a/pkgs/watcher/test/directory_watcher/linux_test.dart +++ b/pkgs/watcher/test/directory_watcher/linux_test.dart @@ -32,15 +32,9 @@ void main() { renameDir('dir/sub', 'sub'); renameDir('sub', 'dir/sub'); - await allowEither(() { - inAnyOrder(withPermutations( - (i, j, k) => isRemoveEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); - - inAnyOrder(withPermutations( - (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); - }, () { - inAnyOrder(withPermutations( - (i, j, k) => isModifyEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); - }); + await inAnyOrder(withPermutations( + (i, j, k) => isRemoveEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); + await inAnyOrder(withPermutations( + (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); }); } diff --git a/pkgs/watcher/test/directory_watcher/mac_os_test.dart b/pkgs/watcher/test/directory_watcher/mac_os_test.dart index ba52d1958..fd2b8277f 100644 --- a/pkgs/watcher/test/directory_watcher/mac_os_test.dart +++ b/pkgs/watcher/test/directory_watcher/mac_os_test.dart @@ -46,16 +46,10 @@ void main() { renameDir('dir/sub', 'sub'); renameDir('sub', 'dir/sub'); - await allowEither(() { - inAnyOrder(withPermutations( - (i, j, k) => isRemoveEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); - - inAnyOrder(withPermutations( - (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); - }, () { - inAnyOrder(withPermutations( - (i, j, k) => isModifyEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); - }); + await inAnyOrder(withPermutations( + (i, j, k) => isRemoveEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); + await inAnyOrder(withPermutations( + (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); }); test('does not suppress files with the same prefix as a directory', () async { // Regression test for https://github.com/dart-lang/watcher/issues/83 diff --git a/pkgs/watcher/test/file_watcher/file_tests.dart b/pkgs/watcher/test/file_watcher/file_tests.dart index 74980f5f6..39434d7a6 100644 --- a/pkgs/watcher/test/file_watcher/file_tests.dart +++ b/pkgs/watcher/test/file_watcher/file_tests.dart @@ -14,10 +14,6 @@ void fileTests({required bool isNative}) { }); test("doesn't notify if the file isn't modified", () async { - // TODO(davidmorgan): fix startup race on MacOS. - if (isNative && Platform.isMacOS) { - await Future.delayed(const Duration(milliseconds: 100)); - } await startWatcher(path: 'file.txt'); await expectNoEvents(); }); diff --git a/pkgs/watcher/test/utils.dart b/pkgs/watcher/test/utils.dart index 354387c56..c42d9b61c 100644 --- a/pkgs/watcher/test/utils.dart +++ b/pkgs/watcher/test/utils.dart @@ -164,62 +164,18 @@ void startClosingEventStream() async { await _watcherEvents.cancel(immediate: true); } -/// A list of [StreamMatcher]s that have been collected using -/// [_collectStreamMatcher]. -List? _collectedStreamMatchers; - -/// Collects all stream matchers that are registered within [block] into a -/// single stream matcher. -/// -/// The returned matcher will match each of the collected matchers in order. -StreamMatcher _collectStreamMatcher(void Function() block) { - var oldStreamMatchers = _collectedStreamMatchers; - var collectedStreamMatchers = _collectedStreamMatchers = []; - try { - block(); - return emitsInOrder(collectedStreamMatchers); - } finally { - _collectedStreamMatchers = oldStreamMatchers; - } -} - -/// Either add [streamMatcher] as an expectation to [_watcherEvents], or collect -/// it with [_collectStreamMatcher]. +/// Add [streamMatcher] as an expectation to [_watcherEvents]. /// /// [streamMatcher] can be a [StreamMatcher], a [Matcher], or a value. -Future _expectOrCollect(Matcher streamMatcher) { - var collectedStreamMatchers = _collectedStreamMatchers; - if (collectedStreamMatchers != null) { - collectedStreamMatchers.add(emits(streamMatcher)); - return Future.sync(() {}); - } else { - return expectLater(_watcherEvents, emits(streamMatcher)); - } +Future _expect(Matcher streamMatcher) { + return expectLater(_watcherEvents, emits(streamMatcher)); } /// Expects that [matchers] will match emitted events in any order. /// /// [matchers] may be [Matcher]s or values, but not [StreamMatcher]s. -Future inAnyOrder(Iterable matchers) { - matchers = matchers.toSet(); - return _expectOrCollect(emitsInAnyOrder(matchers)); -} - -/// Expects that the expectations established in either [block1] or [block2] -/// will match the emitted events. -/// -/// If both blocks match, the one that consumed more events will be used. -Future allowEither(void Function() block1, void Function() block2) => - _expectOrCollect(emitsAnyOf( - [_collectStreamMatcher(block1), _collectStreamMatcher(block2)])); - -/// Allows the expectations established in [block] to match the emitted events. -/// -/// If the expectations in [block] don't match, no error will be raised and no -/// events will be consumed. If this is used at the end of a test, -/// [startClosingEventStream] should be called before it. -Future allowEvents(void Function() block) => - _expectOrCollect(mayEmit(_collectStreamMatcher(block))); +Future inAnyOrder(Iterable matchers) => + _expect(emitsInAnyOrder(matchers.toSet())); /// Returns a StreamMatcher that matches a [WatchEvent] with the given [type] /// and [path]. @@ -274,24 +230,16 @@ Future> takeEvents({required Duration duration}) async { /// Expects that the next event emitted will be for an add event for [path]. Future expectAddEvent(String path) => - _expectOrCollect(isWatchEvent(ChangeType.ADD, path)); + _expect(isWatchEvent(ChangeType.ADD, path)); /// Expects that the next event emitted will be for a modification event for /// [path]. Future expectModifyEvent(String path) => - _expectOrCollect(isWatchEvent(ChangeType.MODIFY, path)); + _expect(isWatchEvent(ChangeType.MODIFY, path)); /// Expects that the next event emitted will be for a removal event for [path]. Future expectRemoveEvent(String path) => - _expectOrCollect(isWatchEvent(ChangeType.REMOVE, path)); - -/// Consumes a modification event for [path] if one is emitted at this point in -/// the schedule, but doesn't throw an error if it isn't. -/// -/// If this is used at the end of a test, [startClosingEventStream] should be -/// called before it. -Future allowModifyEvent(String path) => - _expectOrCollect(mayEmit(isWatchEvent(ChangeType.MODIFY, path))); + _expect(isWatchEvent(ChangeType.REMOVE, path)); /// Track a fake timestamp to be used when writing files. This always increases /// so that files that are deleted and re-created do not have their timestamp From 166c3867296574e3b1cec0eaa9ad76de732af540 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Thu, 16 Oct 2025 13:45:02 +0200 Subject: [PATCH 2/2] Add a way to run tests many times in parallel. --- pkgs/watcher/test/directory_watcher/file_tests.dart | 6 ++++++ pkgs/watcher/test/directory_watcher/link_tests.dart | 6 ++++++ pkgs/watcher/test/file_watcher/file_tests.dart | 6 ++++++ pkgs/watcher/test/file_watcher/link_tests.dart | 6 ++++++ pkgs/watcher/test/utils.dart | 3 +++ 5 files changed, 27 insertions(+) diff --git a/pkgs/watcher/test/directory_watcher/file_tests.dart b/pkgs/watcher/test/directory_watcher/file_tests.dart index 2ce9bb8aa..7e331a01d 100644 --- a/pkgs/watcher/test/directory_watcher/file_tests.dart +++ b/pkgs/watcher/test/directory_watcher/file_tests.dart @@ -12,6 +12,12 @@ import 'package:watcher/src/utils.dart'; import '../utils.dart'; void fileTests() { + for (var i = 0; i != runsPerTest; ++i) { + _fileTests(); + } +} + +void _fileTests() { test('does not notify for files that already exist when started', () async { // Make some pre-existing files. writeFile('a.txt'); diff --git a/pkgs/watcher/test/directory_watcher/link_tests.dart b/pkgs/watcher/test/directory_watcher/link_tests.dart index f4919d1fb..c323c9e54 100644 --- a/pkgs/watcher/test/directory_watcher/link_tests.dart +++ b/pkgs/watcher/test/directory_watcher/link_tests.dart @@ -9,6 +9,12 @@ import 'package:test/test.dart'; import '../utils.dart'; void linkTests({required bool isNative}) { + for (var i = 0; i != runsPerTest; ++i) { + _linkTests(isNative: isNative); + } +} + +void _linkTests({required bool isNative}) { test('notifies when a link is added', () async { createDir('targets'); createDir('links'); diff --git a/pkgs/watcher/test/file_watcher/file_tests.dart b/pkgs/watcher/test/file_watcher/file_tests.dart index 39434d7a6..49d143879 100644 --- a/pkgs/watcher/test/file_watcher/file_tests.dart +++ b/pkgs/watcher/test/file_watcher/file_tests.dart @@ -13,6 +13,12 @@ void fileTests({required bool isNative}) { writeFile('file.txt'); }); + for (var i = 0; i != runsPerTest; ++i) { + _fileTests(isNative: isNative); + } +} + +void _fileTests({required bool isNative}) { test("doesn't notify if the file isn't modified", () async { await startWatcher(path: 'file.txt'); await expectNoEvents(); diff --git a/pkgs/watcher/test/file_watcher/link_tests.dart b/pkgs/watcher/test/file_watcher/link_tests.dart index 8cb5e102d..ddab12c47 100644 --- a/pkgs/watcher/test/file_watcher/link_tests.dart +++ b/pkgs/watcher/test/file_watcher/link_tests.dart @@ -12,6 +12,12 @@ void linkTests({required bool isNative}) { writeLink(link: 'link.txt', target: 'target.txt'); }); + for (var i = 0; i != runsPerTest; ++i) { + _linkTests(isNative: isNative); + } +} + +void _linkTests({required bool isNative}) { test("doesn't notify if nothing is modified", () async { await startWatcher(path: 'link.txt'); await expectNoEvents(); diff --git a/pkgs/watcher/test/utils.dart b/pkgs/watcher/test/utils.dart index c42d9b61c..9a41b5ba6 100644 --- a/pkgs/watcher/test/utils.dart +++ b/pkgs/watcher/test/utils.dart @@ -12,6 +12,9 @@ import 'package:test_descriptor/test_descriptor.dart' as d; import 'package:watcher/src/stat.dart'; import 'package:watcher/watcher.dart'; +/// Edit this to run fast-running tests many times. +int runsPerTest = 1; + typedef WatcherFactory = Watcher Function(String directory); /// Sets the function used to create the watcher.