Skip to content

Commit 1b499c3

Browse files
authored
Add way to run e2e test as a standalone binary. (#2252)
1 parent 30c418b commit 1b499c3

File tree

6 files changed

+107
-57
lines changed

6 files changed

+107
-57
lines changed

pkgs/watcher/test/directory_watcher/client_simulator.dart

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import 'dart:async';
66
import 'dart:io';
77

8-
import 'package:test/test.dart';
98
import 'package:watcher/watcher.dart';
109

1110
/// Simulates a typical use case for `package:watcher`.
@@ -16,6 +15,7 @@ import 'package:watcher/watcher.dart';
1615
/// lengths on disk.
1716
class ClientSimulator {
1817
final Watcher watcher;
18+
final void Function(String) printOnFailure;
1919

2020
/// Events and actions, for logging on failure.
2121
final List<String> messages = [];
@@ -25,14 +25,17 @@ class ClientSimulator {
2525
StreamSubscription<WatchEvent>? _subscription;
2626
DateTime _lastEventAt = DateTime.now();
2727

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

3030
/// Creates a `ClientSimulator` watching with [watcher].
3131
///
3232
/// When returned, it has already read the filesystem state and started
3333
/// tracking file lengths using watcher events.
34-
static Future<ClientSimulator> watch(Watcher watcher) async {
35-
final result = ClientSimulator._(watcher);
34+
static Future<ClientSimulator> watch(
35+
{required Watcher watcher,
36+
required void Function(String) printOnFailure}) async {
37+
final result =
38+
ClientSimulator._(watcher: watcher, printOnFailure: printOnFailure);
3639
result._initialRead();
3740
result._subscription = watcher.events.listen(result._handleEvent);
3841
await watcher.ready;
@@ -121,9 +124,9 @@ class ClientSimulator {
121124

122125
/// Returns whether tracked state matches actual state on disk.
123126
///
124-
/// If not, and [log] is `true`, prints an explanation of the difference
125-
/// with `printOnFailure`.
126-
bool verify({required bool log}) {
127+
/// If not, and [printOnFailure] is passed, uses it to print a dscription of
128+
/// the failure.
129+
bool verify({void Function(String)? printOnFailure}) {
127130
final fileLengths = _readFileLengths();
128131

129132
var result = true;
@@ -133,7 +136,7 @@ class ClientSimulator {
133136
if (unexpectedFiles.isNotEmpty) {
134137
result = false;
135138

136-
if (log) {
139+
if (printOnFailure != null) {
137140
printOnFailure('Failed, on disk but not tracked:');
138141
printOnFailure(
139142
unexpectedFiles.map((path) => path.padLeft(4)).join('\n'));
@@ -144,7 +147,7 @@ class ClientSimulator {
144147
_trackedFileLengths.keys.toSet().difference(fileLengths.keys.toSet());
145148
if (missingExpectedFiles.isNotEmpty) {
146149
result = false;
147-
if (log) {
150+
if (printOnFailure != null) {
148151
printOnFailure('Failed, tracked but not on disk:');
149152
printOnFailure(
150153
missingExpectedFiles.map((path) => path.padLeft(4)).join('\n'));
@@ -160,7 +163,7 @@ class ClientSimulator {
160163
}
161164
if (differentFiles.isNotEmpty) {
162165
result = false;
163-
if (log) {
166+
if (printOnFailure != null) {
164167
printOnFailure('Failed, tracking is out of date:');
165168
final output = StringBuffer();
166169
for (final path in differentFiles) {
Lines changed: 90 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
4+
// ignore_for_file: unreachable_from_main
45

56
import 'dart:io';
67

78
import 'package:path/path.dart' as p;
8-
import 'package:test/test.dart';
9+
import 'package:test/test.dart' as package_test;
10+
import 'package:watcher/watcher.dart';
911

10-
import '../utils.dart';
12+
import '../utils.dart' as utils;
1113
import 'client_simulator.dart';
1214
import 'file_changer.dart';
1315

@@ -16,56 +18,101 @@ import 'file_changer.dart';
1618
///
1719
/// The test passes if the [ClientSimulator] tracking matches what's actually on
1820
/// disk.
21+
void endToEndTests() {
22+
package_test.test('end to end test',
23+
timeout: const package_test.Timeout(Duration(minutes: 5)), () async {
24+
await _runTest();
25+
});
26+
}
27+
28+
/// Runs the test.
1929
///
20-
/// Fails on Linux due to https://github.com/dart-lang/tools/issues/2228.
30+
/// To run without `package:test`, pass [addTearDown], [createWatcher], [fail]
31+
/// and [printOnFailure] replacements.
2132
///
22-
/// Fails sometimes on Windows due to
23-
/// https://github.com/dart-lang/tools/issues/2234.
24-
void endToEndTests({required bool isNative}) {
25-
test('end to end test', timeout: const Timeout(Duration(minutes: 5)),
26-
() async {
27-
final temp = Directory.systemTemp.createTempSync();
28-
addTearDown(() => temp.deleteSync(recursive: true));
33+
/// To run until failure, set [endlessMode] to `true`.
34+
Future<void> _runTest({
35+
void Function(void Function())? addTearDown,
36+
Watcher Function({required String path})? createWatcher,
37+
void Function(String)? fail,
38+
void Function(String)? printOnFailure,
39+
bool endlessMode = false,
40+
}) async {
41+
addTearDown ??= package_test.addTearDown;
42+
createWatcher ??= utils.createWatcher;
43+
fail ??= package_test.fail;
44+
printOnFailure ??= package_test.printOnFailure;
2945

30-
// Create the watcher and [ClientSimulator].
31-
final watcher = createWatcher(path: temp.path);
32-
final client = await ClientSimulator.watch(watcher);
33-
addTearDown(client.close);
46+
final temp = Directory.systemTemp.createTempSync();
47+
addTearDown(() => temp.deleteSync(recursive: true));
3448

35-
// 20 iterations of making changes, waiting for events to settle, and
36-
// checking for consistency.
37-
final changer = FileChanger(temp.path);
38-
for (var i = 0; i != 40; ++i) {
39-
for (final entity in temp.listSync()) {
40-
entity.deleteSync(recursive: true);
41-
}
42-
// File changes.
43-
final messages = await changer.changeFiles(times: 200);
49+
// Create the watcher and [ClientSimulator].
50+
final watcher = createWatcher(path: temp.path);
51+
final client = await ClientSimulator.watch(
52+
watcher: watcher, printOnFailure: printOnFailure);
53+
addTearDown(client.close);
4454

45-
// Give time for events to arrive. To allow tests to run quickly when the
46-
// events are handled quickly, poll and continue if verification passes.
47-
for (var waits = 0; waits != 20; ++waits) {
48-
if (client.verify(log: false)) {
49-
break;
50-
}
51-
await client.waitForNoEvents(const Duration(milliseconds: 100));
55+
// 40 iterations of making changes, waiting for events to settle, and
56+
// checking for consistency.
57+
final changer = FileChanger(temp.path);
58+
for (var i = 0; endlessMode || i != 40; ++i) {
59+
if (endlessMode) stdout.write('.');
60+
for (final entity in temp.listSync()) {
61+
entity.deleteSync(recursive: true);
62+
}
63+
// File changes.
64+
final messages = await changer.changeFiles(times: 200);
65+
66+
// Give time for events to arrive. To allow tests to run quickly when the
67+
// events are handled quickly, poll and continue if verification passes.
68+
var succeeded = false;
69+
for (var waits = 0; waits != 20; ++waits) {
70+
if (client.verify()) {
71+
succeeded = true;
72+
break;
5273
}
74+
await client.waitForNoEvents(const Duration(milliseconds: 100));
75+
}
5376

54-
// Verify for real and fail the test if still not consistent.
55-
if (!client.verify(log: true)) {
56-
// Write the file operations before the failure to a log, fail the test.
57-
final logTemp = Directory.systemTemp.createTempSync();
58-
final fileChangesLogPath = p.join(logTemp.path, 'changes.txt');
59-
File(fileChangesLogPath)
60-
.writeAsStringSync(messages.map((m) => '$m\n').join(''));
61-
final clientLogPath = p.join(logTemp.path, 'client.txt');
62-
File(clientLogPath)
63-
.writeAsStringSync(client.messages.map((m) => '$m\n').join(''));
64-
fail('''
77+
// Fail the test if still not consistent.
78+
if (!succeeded) {
79+
if (endlessMode) print('');
80+
client.verify(printOnFailure: printOnFailure);
81+
// Write the file operations before the failure to a log, fail the test.
82+
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(''));
89+
fail('''
6590
Failed on run $i.
6691
Files changes: $fileChangesLogPath
6792
Client log: $clientLogPath''');
68-
}
6993
}
70-
});
94+
}
95+
}
96+
97+
/// Main method for running the e2e test without `package:test`.
98+
///
99+
/// Exits on failure, or runs forever.
100+
Future<void> main() async {
101+
final teardowns = <void Function()>[];
102+
try {
103+
await _runTest(
104+
addTearDown: teardowns.add,
105+
createWatcher: ({required String path}) => DirectoryWatcher(path),
106+
fail: (message) {
107+
print(message);
108+
exit(1);
109+
},
110+
printOnFailure: print,
111+
endlessMode: true,
112+
);
113+
} finally {
114+
for (final teardown in teardowns) {
115+
teardown();
116+
}
117+
}
71118
}

pkgs/watcher/test/directory_watcher/linux_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ void main() {
1919

2020
fileTests(isNative: true);
2121
linkTests(isNative: true);
22-
endToEndTests(isNative: true);
22+
endToEndTests();
2323

2424
test('DirectoryWatcher creates a LinuxDirectoryWatcher on Linux', () {
2525
expect(DirectoryWatcher('.'), const TypeMatcher<LinuxDirectoryWatcher>());

pkgs/watcher/test/directory_watcher/mac_os_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ void main() {
1919

2020
fileTests(isNative: true);
2121
linkTests(isNative: true);
22-
endToEndTests(isNative: true);
22+
endToEndTests();
2323

2424
test('DirectoryWatcher creates a MacOSDirectoryWatcher on Mac OS', () {
2525
expect(DirectoryWatcher('.'), const TypeMatcher<MacOSDirectoryWatcher>());

pkgs/watcher/test/directory_watcher/polling_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ void main() {
2424

2525
fileTests(isNative: false);
2626
linkTests(isNative: false);
27-
endToEndTests(isNative: false);
27+
endToEndTests();
2828

2929
// A poll does an async directory list that runs "stat" on each file. Check
3030
// handling of a file that is deleted between the two.

pkgs/watcher/test/directory_watcher/windows_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ void main() {
2424

2525
fileTests(isNative: true);
2626
linkTests(isNative: true);
27-
endToEndTests(isNative: true);
27+
endToEndTests();
2828

2929
test('DirectoryWatcher creates a WindowsDirectoryWatcher on Windows', () {
3030
expect(DirectoryWatcher('.'), const TypeMatcher<WindowsDirectoryWatcher>());

0 commit comments

Comments
 (0)