Skip to content

Commit 31b4885

Browse files
authored
Improve watcher e2e test. (#2254)
1 parent 1b499c3 commit 31b4885

File tree

6 files changed

+77
-31
lines changed

6 files changed

+77
-31
lines changed

pkgs/watcher/lib/src/directory_watcher/linux.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ class _LinuxDirectoryWatcher
150150

151151
/// The callback that's run when a batch of changes comes in.
152152
void _onBatch(List<Event> batch) {
153+
logForTesting?.call('_onBatch,$batch');
153154
var files = <String>{};
154155
var dirs = <String>{};
155156
var changed = <String>{};
@@ -281,6 +282,7 @@ class _LinuxDirectoryWatcher
281282
// Only emit ADD if it hasn't already been emitted due to the file being
282283
// modified or added after the directory was added.
283284
if (!_files.contains(entity.path)) {
285+
logForTesting?.call('_addSubdir,$path,$entity');
284286
_files.add(entity.path);
285287
_emitEvent(ChangeType.ADD, entity.path);
286288
}
@@ -346,6 +348,8 @@ class _LinuxDirectoryWatcher
346348
///
347349
/// See [_Watch] class comment.
348350
_Watch _watch(String path) {
351+
logForTesting?.call('_Watch._watch,$path');
352+
349353
_watches[path]?.cancel();
350354
final result = _Watch(path, _cancelWatchesUnderPath);
351355
_watches[path] = result;
@@ -360,6 +364,8 @@ class _LinuxDirectoryWatcher
360364

361365
/// Cancels all watches under path [path].
362366
void _cancelWatchesUnderPath(String path) {
367+
logForTesting?.call('_Watch.cancelWatchesUnderPath,$path');
368+
363369
// If [path] is the root watch directory do nothing, that's handled when the
364370
// stream closes.
365371
if (path == this.path) return;
@@ -397,6 +403,7 @@ class _Watch {
397403
String path, StreamController<FileSystemEvent> controller) {
398404
return Directory(path).watch().listen(
399405
(event) {
406+
logForTesting?.call('_Watch._listen,$path,$event');
400407
if (event is FileSystemDeleteEvent ||
401408
(event.isDirectory && event is FileSystemMoveEvent)) {
402409
_cancelWatchesUnderPath(event.path);
@@ -410,6 +417,7 @@ class _Watch {
410417
}
411418

412419
void cancel() {
420+
logForTesting?.call('_Watch.cancel,$path');
413421
_subscription.cancel();
414422
}
415423
}

pkgs/watcher/lib/src/directory_watcher/mac_os.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ class _MacOSDirectoryWatcher
112112

113113
/// The callback that's run when [Directory.watch] emits a batch of events.
114114
void _onBatch(List<Event> batch) {
115+
logForTesting?.call('onBatch: $batch');
116+
115117
// If we get a batch of events before we're ready to begin emitting events,
116118
// it's probable that it's a batch of pre-watcher events (see issue 14373).
117119
// Ignore those events and re-list the directory.

pkgs/watcher/lib/src/utils.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,6 @@ extension IgnoringError<T> on Stream<T> {
7272
));
7373
}
7474
}
75+
76+
/// Set to log watcher internals for testing.
77+
void Function(String)? logForTesting;

pkgs/watcher/test/directory_watcher/client_simulator.dart

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,22 @@ import 'package:watcher/watcher.dart';
1515
/// lengths on disk.
1616
class ClientSimulator {
1717
final Watcher watcher;
18-
final void Function(String) printOnFailure;
19-
20-
/// Events and actions, for logging on failure.
21-
final List<String> messages = [];
18+
final void Function(String) log;
2219

2320
final Map<String, int> _trackedFileLengths = {};
2421

2522
StreamSubscription<WatchEvent>? _subscription;
2623
DateTime _lastEventAt = DateTime.now();
2724

28-
ClientSimulator._({required this.watcher, required this.printOnFailure});
25+
ClientSimulator._({required this.watcher, required this.log});
2926

3027
/// Creates a `ClientSimulator` watching with [watcher].
3128
///
3229
/// When returned, it has already read the filesystem state and started
3330
/// tracking file lengths using watcher events.
3431
static Future<ClientSimulator> watch(
35-
{required Watcher watcher,
36-
required void Function(String) printOnFailure}) async {
37-
final result =
38-
ClientSimulator._(watcher: watcher, printOnFailure: printOnFailure);
32+
{required Watcher watcher, required void Function(String) log}) async {
33+
final result = ClientSimulator._(watcher: watcher, log: log);
3934
result._initialRead();
4035
result._subscription = watcher.events.listen(result._handleEvent);
4136
await watcher.ready;
@@ -93,7 +88,7 @@ class ClientSimulator {
9388
if (_trackedFileLengths.containsKey(event.path)) {
9489
// This happens sometimes, so investigation+fix would be needed
9590
// if we want to make it an error.
96-
printOnFailure('Warning: ADD for tracked path,${event.path}');
91+
log('Warning: ADD for tracked path,${event.path}');
9792
}
9893
_readFile(event.path);
9994
break;
@@ -106,7 +101,7 @@ class ClientSimulator {
106101
if (!_trackedFileLengths.containsKey(event.path)) {
107102
// This happens sometimes, so investigation+fix would be needed
108103
// if we want to make it an error.
109-
printOnFailure('Warning: REMOVE untracked path: ${event.path}');
104+
log('Warning: REMOVE untracked path: ${event.path}');
110105
}
111106
_trackedFileLengths.remove(event.path);
112107
break;
@@ -182,6 +177,6 @@ class ClientSimulator {
182177
// Remove the tmp folder from the message.
183178
message =
184179
message.replaceAll('${watcher.path}${Platform.pathSeparator}', '');
185-
messages.add(message);
180+
log(message);
186181
}
187182
}

pkgs/watcher/test/directory_watcher/end_to_end_tests.dart

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'dart:io';
77

88
import 'package:path/path.dart' as p;
99
import 'package:test/test.dart' as package_test;
10+
import 'package:watcher/src/utils.dart';
1011
import 'package:watcher/watcher.dart';
1112

1213
import '../utils.dart' as utils;
@@ -31,12 +32,16 @@ void endToEndTests() {
3132
/// and [printOnFailure] replacements.
3233
///
3334
/// To run until failure, set [endlessMode] to `true`.
35+
///
36+
/// To fix the seed, set [seed]. The failure message prints the seed, so this
37+
/// can be used to run just the events that triggered the failure.
3438
Future<void> _runTest({
3539
void Function(void Function())? addTearDown,
3640
Watcher Function({required String path})? createWatcher,
3741
void Function(String)? fail,
3842
void Function(String)? printOnFailure,
3943
bool endlessMode = false,
44+
int? seed,
4045
}) async {
4146
addTearDown ??= package_test.addTearDown;
4247
createWatcher ??= utils.createWatcher;
@@ -46,22 +51,28 @@ Future<void> _runTest({
4651
final temp = Directory.systemTemp.createTempSync();
4752
addTearDown(() => temp.deleteSync(recursive: true));
4853

54+
// Turn on logging of the watchers.
55+
final log = <LogEntry>[];
56+
logForTesting = (message) => log.add(LogEntry('W $message'));
57+
4958
// Create the watcher and [ClientSimulator].
5059
final watcher = createWatcher(path: temp.path);
5160
final client = await ClientSimulator.watch(
52-
watcher: watcher, printOnFailure: printOnFailure);
61+
watcher: watcher, log: (message) => log.add(LogEntry('C $message')));
5362
addTearDown(client.close);
5463

5564
// 40 iterations of making changes, waiting for events to settle, and
5665
// checking for consistency.
5766
final changer = FileChanger(temp.path);
5867
for (var i = 0; endlessMode || i != 40; ++i) {
68+
final runSeed = seed ?? i;
69+
log.clear();
5970
if (endlessMode) stdout.write('.');
6071
for (final entity in temp.listSync()) {
6172
entity.deleteSync(recursive: true);
6273
}
6374
// File changes.
64-
final messages = await changer.changeFiles(times: 200);
75+
log.addAll(await changer.changeFiles(times: 200, seed: runSeed));
6576

6677
// Give time for events to arrive. To allow tests to run quickly when the
6778
// events are handled quickly, poll and continue if verification passes.
@@ -80,24 +91,30 @@ Future<void> _runTest({
8091
client.verify(printOnFailure: printOnFailure);
8192
// Write the file operations before the failure to a log, fail the test.
8293
final logTemp = Directory.systemTemp.createTempSync();
83-
final fileChangesLogPath = p.join(logTemp.path, 'changes.txt');
84-
File(fileChangesLogPath)
85-
.writeAsStringSync(messages.map((m) => '$m\n').join(''));
86-
final clientLogPath = p.join(logTemp.path, 'client.txt');
87-
File(clientLogPath)
88-
.writeAsStringSync(client.messages.map((m) => '$m\n').join(''));
94+
final logPath = p.join(logTemp.path, 'log.txt');
95+
96+
// Sort the log entries by timestamp.
97+
log.sort();
98+
99+
File(logPath).writeAsStringSync(log.map((m) => '$m\n').join(''));
89100
fail('''
90-
Failed on run $i.
91-
Files changes: $fileChangesLogPath
92-
Client log: $clientLogPath''');
101+
Failed on run $i, seed $runSeed. Run in a loop with that seed using:
102+
103+
dart test/directory_watcher/end_to_end_tests.dart $runSeed
104+
105+
Changes/watcher/client log: $logPath
106+
''');
93107
}
94108
}
95109
}
96110

97111
/// Main method for running the e2e test without `package:test`.
98112
///
113+
/// Optionally, pass the seed to run with as the only argument.
114+
///
99115
/// Exits on failure, or runs forever.
100-
Future<void> main() async {
116+
Future<void> main(List<String> arguments) async {
117+
final seed = arguments.isNotEmpty ? int.parse(arguments.first) : null;
101118
final teardowns = <void Function()>[];
102119
try {
103120
await _runTest(
@@ -109,10 +126,28 @@ Future<void> main() async {
109126
},
110127
printOnFailure: print,
111128
endlessMode: true,
129+
seed: seed,
112130
);
113131
} finally {
114132
for (final teardown in teardowns) {
115133
teardown();
116134
}
117135
}
118136
}
137+
138+
/// Log entry with timestamp.
139+
///
140+
/// Because file events happen on a different isolate the merged log uses
141+
/// timestamps to put entries in the correct order.
142+
class LogEntry implements Comparable<LogEntry> {
143+
final DateTime timestamp;
144+
final String message;
145+
146+
LogEntry(this.message) : timestamp = DateTime.now();
147+
148+
@override
149+
int compareTo(LogEntry other) => timestamp.compareTo(other.timestamp);
150+
151+
@override
152+
String toString() => message;
153+
}

pkgs/watcher/test/directory_watcher/file_changer.dart

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'dart:math';
99
import 'package:path/path.dart' as p;
1010

1111
import '../utils.dart';
12+
import 'end_to_end_tests.dart';
1213

1314
/// Changes files randomly.
1415
///
@@ -26,22 +27,24 @@ class FileChanger {
2627
final String path;
2728

2829
Random _random = Random(0);
29-
final List<String> _messages = [];
30+
final List<LogEntry> _messages = [];
3031

3132
FileChanger(this.path);
3233

3334
/// Changes files under [path], [times] times.
3435
///
36+
/// Changes are randomized with [seed], pass the same value to get the same
37+
/// changes.
38+
///
3539
/// Returns a log of the changes made.
36-
Future<List<String>> changeFiles({required int times}) async {
40+
Future<List<LogEntry>> changeFiles(
41+
{required int times, required int seed}) async {
42+
_random = Random(seed);
3743
final result = await Isolate.run(() => _changeFiles(times: times));
38-
// The `Random` instance gets copied to the isolate on every run, so by
39-
// default it will produce the same numbers. Update it to get new numbers.
40-
_random = Random(_random.nextInt(0xffffffff));
4144
return result;
4245
}
4346

44-
Future<List<String>> _changeFiles({required int times}) async {
47+
Future<List<LogEntry>> _changeFiles({required int times}) async {
4548
_messages.clear();
4649
for (var i = 0; i != times; ++i) {
4750
await _changeFilesOnce();
@@ -163,6 +166,6 @@ class FileChanger {
163166
void _log(String message) {
164167
// Remove the tmp folder from the message.
165168
message = message.replaceAll(',$path${Platform.pathSeparator}', ',');
166-
_messages.add(message);
169+
_messages.add(LogEntry('F $message'));
167170
}
168171
}

0 commit comments

Comments
 (0)