Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions pkgs/watcher/test/directory_watcher/client_simulator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// 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:test/test.dart';
import 'package:watcher/watcher.dart';

/// Simulates a typical use case for `package:watcher`.
///
/// Tracks file lengths, updating based on watch events.
///
/// Call [verify] to verify whether the tracked lengths match the actual file
/// lengths on disk.
class ClientSimulator {
final Watcher watcher;

/// Events and actions, for logging on failure.
final List<String> messages = [];

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

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

ClientSimulator._(this.watcher);

/// Creates a `ClientSimulator` watching with [watcher].
///
/// When returned, it has already read the filesystem state and started
/// tracking file lengths using watcher events.
static Future<ClientSimulator> watch(Watcher watcher) async {
final result = ClientSimulator._(watcher);
result._initialRead();
result._subscription = watcher.events.listen(result._handleEvent);
await watcher.ready;
return result;
}

/// Waits for at least [duration], and for a span of that duration in which no
/// events are received.
Future<void> waitForNoEvents(Duration duration) async {
_lastEventAt = DateTime.now();
while (true) {
final timeLeft = duration - DateTime.now().difference(_lastEventAt);
if (timeLeft <= Duration.zero) return;
await Future<void>.delayed(timeLeft + const Duration(milliseconds: 1));
}
}

/// Closes the watcher subscription.
void close() {
_subscription?.cancel();
}

Directory get _directory => Directory(watcher.path);

/// Reads all files to get the start state.
void _initialRead() {
for (final file in _directory.listSync(recursive: true).whereType<File>()) {
_readFile(file.path);
}
}

/// Reads the file at [path] and updates tracked state with its current
/// length.
///
/// If the file cannot be read the size is set to -1, this can be corrected
/// by a REMOVE event.
void _readFile(String path) {
try {
_trackedFileLengths[path] = File(path).lengthSync();
} catch (_) {
_trackedFileLengths[path] = -1;
}
}

/// Updates tracked state for [event].
///
/// For add and modify events, reads the file to determine its length.
///
/// For remove events, removes tracking for that file.
void _handleEvent(WatchEvent event) {
_log(event.toString());
_lastEventAt = DateTime.now();
switch (event.type) {
case ChangeType.ADD:
if (_trackedFileLengths.containsKey(event.path)) {
// This happens sometimes, so investigation+fix would be needed
// if we want to make it an error.
printOnFailure('Warning: ADD for tracked path,${event.path}');
}
_readFile(event.path);
break;

case ChangeType.MODIFY:
_readFile(event.path);
break;

case ChangeType.REMOVE:
if (!_trackedFileLengths.containsKey(event.path)) {
// This happens sometimes, so investigation+fix would be needed
// if we want to make it an error.
printOnFailure('Warning: REMOVE untracked path: ${event.path}');
}
_trackedFileLengths.remove(event.path);
break;
}
}

/// Reads current file lengths for verification.
Map<String, int> _readFileLengths() {
final result = <String, int>{};
for (final file in _directory.listSync(recursive: true).whereType<File>()) {
result[file.path] = file.lengthSync();
}
return result;
}

/// Returns whether tracked state matches actual state on disk.
///
/// If not, and [log] is `true`, prints an explanation of the difference
/// with `printOnFailure`.
bool verify({required bool log}) {
final fileLengths = _readFileLengths();

var result = true;

final unexpectedFiles =
fileLengths.keys.toSet().difference(_trackedFileLengths.keys.toSet());
if (unexpectedFiles.isNotEmpty) {
result = false;

if (log) {
printOnFailure('Failed, on disk but not tracked:');
printOnFailure(
unexpectedFiles.map((path) => path.padLeft(4)).join('\n'));
}
}

final missingExpectedFiles =
_trackedFileLengths.keys.toSet().difference(fileLengths.keys.toSet());
if (missingExpectedFiles.isNotEmpty) {
result = false;
if (log) {
printOnFailure('Failed, tracked but not on disk:');
printOnFailure(
missingExpectedFiles.map((path) => path.padLeft(4)).join('\n'));
}
}

final differentFiles = <String>{};
for (final path in fileLengths.keys) {
if (_trackedFileLengths[path] == null) continue;
if (fileLengths[path] != _trackedFileLengths[path]) {
differentFiles.add(path);
}
}
if (differentFiles.isNotEmpty) {
result = false;
if (log) {
printOnFailure('Failed, tracking is out of date:');
final output = StringBuffer();
for (final path in differentFiles) {
final tracked = _trackedFileLengths[path]!;
final actual = fileLengths[path]!;
output.write(' $path tracked=$tracked actual=$actual\n');
}
printOnFailure(output.toString());
}
}

return result;
}

void _log(String message) {
// Remove the tmp folder from the message.
message =
message.replaceAll('${watcher.path}${Platform.pathSeparator}', '');
messages.add(message);
}
}
85 changes: 85 additions & 0 deletions pkgs/watcher/test/directory_watcher/end_to_end_tests.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// 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:io';

import 'package:path/path.dart' as p;
import 'package:test/test.dart';

import '../utils.dart';
import 'client_simulator.dart';
import 'file_changer.dart';

/// End to end test using a [FileChanger] that randomly changes files, then a
/// [ClientSimulator] that tracks state using a Watcher.
///
/// The test passes if the [ClientSimulator] tracking matches what's actually on
/// disk.
///
/// Fails on Linux due to https://github.com/dart-lang/tools/issues/2228.
///
/// Fails sometimes on Windows due to
/// https://github.com/dart-lang/tools/issues/2234.
void endToEndTests({required bool isNative}) {
test('end to end test', timeout: const Timeout(Duration(minutes: 5)),
() async {
final temp = Directory.systemTemp.createTempSync();
addTearDown(() => temp.deleteSync(recursive: true));

// Start with some files.
final changer = FileChanger(temp.path);
await changer.changeFiles(times: 100);

// Create the watcher and [ClientSimulator].
final watcher = createWatcher(path: temp.path);
final client = await ClientSimulator.watch(watcher);
addTearDown(client.close);

// 20 iterations of making changes, waiting for events to settle, and
// checking for consistency.
for (var i = 0; i != 20; ++i) {
// File changes.
final messages = await changer.changeFiles(times: 100);

// Give time for events to arrive. To allow tests to run quickly when the
// events are handled quickly, poll and continue if verification passes.
for (var waits = 0; waits != 20; ++waits) {
if (client.verify(log: false)) {
break;
}
await client.waitForNoEvents(const Duration(milliseconds: 100));
}

// Verify for real and fail the test if still not consistent.
if (!client.verify(log: true)) {
if (Platform.isLinux && isNative) {
print('Ignoring expected failure for Linux native watcher.');
return;
}
if (Platform.isWindows && isNative) {
print('Ignoring expected failure for Windows native watcher.');
return;
}

// Write the file operations before the failure to a log, fail the test.
final logTemp = Directory.systemTemp.createTempSync();
final fileChangesLogPath = p.join(logTemp.path, 'changes.txt');
File(fileChangesLogPath)
.writeAsStringSync(messages.map((m) => '$m\n').join(''));
final clientLogPath = p.join(logTemp.path, 'client.txt');
File(clientLogPath)
.writeAsStringSync(client.messages.map((m) => '$m\n').join(''));
fail('''
Failed on run $i.
Files changes: $fileChangesLogPath
Client log: $clientLogPath''');
}
}

if (Platform.isLinux && isNative) {
fail('Expected Linux native watcher failure, but test passed!');
}
// Can't expect the Windows failure as it does sometimes succeed.
});
}
Loading