| // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:async'; | |
| import 'dart:io'; | |
| import 'package:clock/clock.dart'; | |
| import 'package:fake_async/fake_async.dart'; | |
| import 'package:test/test.dart'; | |
| import 'package:watcher/src/event_batching.dart'; | |
| import 'package:watcher/src/testing.dart'; | |
| void main() { | |
| group('batchAndConvertEvents', () { | |
| setUp(() { | |
| overridableDateTimeNow = () => clock.now(); | |
| }); | |
| tearDown(() { | |
| overridableDateTimeNow = DateTime.now; | |
| }); | |
| group('without buffering', () { | |
| test('splits into expected batches', () { | |
| var expectationsRan = false; | |
| fakeAsync((async) { | |
| final controller = StreamController<FileSystemEvent>(); | |
| final stream = controller.stream | |
| .batchNearbyMicrotasksAndConvertEvents(); | |
| final batchesFuture = stream.toList(); | |
| // Send events in ten batches of size 1, 2, 3, ..., 10. | |
| for (var i = 0; i != 10; ++i) { | |
| for (var j = 0; j != i + 1; ++j) { | |
| controller.add(FileSystemCreateEvent('$i,$j', true)); | |
| } | |
| async.elapse(const Duration(milliseconds: 1)); | |
| } | |
| controller.close(); | |
| batchesFuture.then((batches) { | |
| // Check for the exact expected batches. | |
| for (var i = 0; i != 10; ++i) { | |
| expect(batches[i].length, i + 1); | |
| for (var j = 0; j != i + 1; ++j) { | |
| expect(batches[i][j].path, '$i,$j'); | |
| } | |
| } | |
| expectationsRan = true; | |
| }); | |
| // Cause `batchesFuture` to complete. | |
| async.flushMicrotasks(); | |
| }); | |
| // Expectations are at the end of a fake async future, check it actually | |
| // completed. | |
| expect(expectationsRan, true); | |
| }); | |
| }); | |
| group('buffered by path', () { | |
| test('splits into expected batches', () { | |
| var expecationsRan = false; | |
| fakeAsync((async) { | |
| final controller = StreamController<FileSystemEvent>(); | |
| final stream = controller.stream.batchBufferedByPathAndConvertEvents( | |
| duration: const Duration(milliseconds: 10), | |
| ); | |
| final batchesFuture = stream.toList(); | |
| controller.add(FileSystemCreateEvent('1', true)); | |
| controller.add(FileSystemCreateEvent('2', true)); | |
| // Don't send "2" again, it should be emitted. | |
| async.elapse(const Duration(milliseconds: 10)); | |
| controller.add(FileSystemCreateEvent('1', true)); | |
| controller.add(FileSystemCreateEvent('3', true)); | |
| controller.add(FileSystemCreateEvent('4', true)); | |
| controller.add(FileSystemCreateEvent('5', true)); | |
| // Don't send "1", "3" or "4" again, they should be emitted. | |
| async.elapse(const Duration(milliseconds: 10)); | |
| controller.add(FileSystemCreateEvent('5', true)); | |
| controller.add(FileSystemCreateEvent('6', true)); | |
| controller.add(FileSystemCreateEvent('7', true)); | |
| controller.add(FileSystemCreateEvent('8', true)); | |
| controller.add(FileSystemCreateEvent('9', true)); | |
| controller.add(FileSystemCreateEvent('10', true)); | |
| // Everything except "9" and "10" should be emitted. | |
| async.elapse(const Duration(milliseconds: 10)); | |
| controller.add(FileSystemCreateEvent('9', true)); | |
| controller.add(FileSystemCreateEvent('10', true)); | |
| // Close of the controller should force emit of "9" with the "10". | |
| async.elapse(const Duration(milliseconds: 10)); | |
| controller.add(FileSystemCreateEvent('9', true)); | |
| controller.close(); | |
| batchesFuture.then((batches) { | |
| expect(batches.map((b) => b.map((e) => e.path).toList()).toList(), [ | |
| ['2'], | |
| ['1', '1', '3', '4'], | |
| ['5', '5', '6', '7', '8'], | |
| ['9', '9', '9', '10', '10'], | |
| ]); | |
| expecationsRan = true; | |
| }); | |
| // Cause `batchesFuture` to complete. | |
| async.flushMicrotasks(); | |
| }); | |
| // Expectations are at the end of a fake async future, check it actually | |
| // completed. | |
| expect(expecationsRan, true); | |
| }); | |
| test('continues batching after pause', () async { | |
| var expectationsRan = false; | |
| fakeAsync((async) { | |
| final controller = StreamController<FileSystemEvent>(); | |
| final stream = controller.stream.batchBufferedByPathAndConvertEvents( | |
| duration: const Duration(milliseconds: 5), | |
| ); | |
| final batchesFuture = stream.toList(); | |
| controller.add(FileSystemCreateEvent('1', true)); | |
| async.elapse(const Duration(milliseconds: 2)); | |
| controller.add(FileSystemCreateEvent('1', true)); | |
| async.elapse(const Duration(milliseconds: 10)); | |
| controller.add(FileSystemCreateEvent('2', true)); | |
| async.elapse(const Duration(milliseconds: 2)); | |
| controller.add(FileSystemCreateEvent('2', true)); | |
| async.elapse(const Duration(milliseconds: 10)); | |
| controller.close(); | |
| batchesFuture.then((batches) { | |
| expect(batches.map((b) => b.map((e) => e.path).toList()).toList(), [ | |
| ['1', '1'], | |
| ['2', '2'], | |
| ]); | |
| expectationsRan = true; | |
| }); | |
| // Cause `batchesFuture` to complete. | |
| async.flushMicrotasks(); | |
| }); | |
| // Expectations are at the end of a fake async future, check it actually | |
| // completed. | |
| expect(expectationsRan, true); | |
| }); | |
| test( | |
| 'converts moves into separate create and delete', | |
| // Move events aren't used on MacOS, so the `Event` conversion rejects | |
| // them. | |
| skip: Platform.isMacOS, | |
| () { | |
| var expectationsRan = false; | |
| fakeAsync((async) { | |
| final controller = StreamController<FileSystemEvent>(); | |
| final stream = controller.stream | |
| .batchBufferedByPathAndConvertEvents( | |
| duration: const Duration(milliseconds: 50), | |
| ); | |
| final batchesFuture = stream.toList(); | |
| // Delete of a, delete of b, create of b, create of c should end up | |
| // in one batch. | |
| controller.add(FileSystemMoveEvent('a', false, 'b')); | |
| async.elapse(const Duration(milliseconds: 1)); | |
| controller.add(FileSystemMoveEvent('b', false, 'c')); | |
| // Then a second batch with delete of c, create of d. | |
| async.elapse(const Duration(milliseconds: 100)); | |
| controller.add(FileSystemMoveEvent('c', false, 'd')); | |
| controller.close(); | |
| batchesFuture.then((batches) { | |
| expect( | |
| batches | |
| .map( | |
| (b) => | |
| b.map((e) => '${e.runtimeType} ${e.path}').toList(), | |
| ) | |
| .toList(), | |
| [ | |
| { | |
| 'FileSystemCreateEvent b', | |
| 'FileSystemCreateEvent c', | |
| 'FileSystemDeleteEvent a', | |
| 'FileSystemDeleteEvent b', | |
| }, | |
| {'FileSystemCreateEvent d', 'FileSystemDeleteEvent c'}, | |
| ], | |
| ); | |
| expectationsRan = true; | |
| }); | |
| // Cause `batchesFuture` to complete. | |
| async.flushMicrotasks(); | |
| }); | |
| // Expectations are at the end of a fake async future, check it | |
| // actually completed. | |
| expect(expectationsRan, true); | |
| }, | |
| ); | |
| }); | |
| }); | |
| } |