|
5 | 5 |
|
6 | 6 | import 'dart:io'; |
7 | 7 |
|
8 | | -import 'package:path/path.dart' as p; |
9 | | -import 'package:test/test.dart' as package_test; |
10 | | -import 'package:watcher/src/utils.dart'; |
11 | | -import 'package:watcher/watcher.dart'; |
| 8 | +import 'package:test/test.dart'; |
12 | 9 |
|
13 | | -import '../utils.dart' as utils; |
14 | 10 | import 'client_simulator.dart'; |
| 11 | +import 'end_to_end_test_runner.dart'; |
15 | 12 | import 'file_changer.dart'; |
16 | 13 |
|
17 | | -/// End to end test using a [FileChanger] that randomly changes files, then a |
| 14 | +/// End to end test using a [FileChanger] to change files then a |
18 | 15 | /// [ClientSimulator] that tracks state using a Watcher. |
19 | 16 | /// |
20 | | -/// The test passes if the [ClientSimulator] tracking matches what's actually on |
| 17 | +/// The tests pass if the [ClientSimulator] tracking matches what's actually on |
21 | 18 | /// disk. |
| 19 | +/// |
| 20 | +/// `end_to_end_test_runner` can be run as a binary to try random file changes |
| 21 | +/// until a failure, it outputs a log which can be turned into a test case here. |
22 | 22 | void endToEndTests() { |
23 | | - package_test.test('end to end test', |
24 | | - timeout: const package_test.Timeout(Duration(minutes: 5)), () async { |
25 | | - await _runTest(); |
| 23 | + // Random test to cover a wide range of cases. |
| 24 | + test('end to end test: random', timeout: const Timeout(Duration(minutes: 5)), |
| 25 | + () async { |
| 26 | + await runTest(name: 'random', repeats: 100); |
26 | 27 | }); |
27 | | -} |
28 | | - |
29 | | -/// Runs the test. |
30 | | -/// |
31 | | -/// To run without `package:test`, pass [addTearDown], [createWatcher], [fail] |
32 | | -/// and [printOnFailure] replacements. |
33 | | -/// |
34 | | -/// 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. |
38 | | -Future<void> _runTest({ |
39 | | - void Function(void Function())? addTearDown, |
40 | | - Watcher Function({required String path})? createWatcher, |
41 | | - void Function(String)? fail, |
42 | | - void Function(String)? printOnFailure, |
43 | | - bool endlessMode = false, |
44 | | - int? seed, |
45 | | -}) async { |
46 | | - addTearDown ??= package_test.addTearDown; |
47 | | - createWatcher ??= utils.createWatcher; |
48 | | - fail ??= package_test.fail; |
49 | | - printOnFailure ??= package_test.printOnFailure; |
50 | | - |
51 | | - final temp = Directory.systemTemp.createTempSync(); |
52 | | - addTearDown(() => temp.deleteSync(recursive: true)); |
53 | | - |
54 | | - // Turn on logging of the watchers. |
55 | | - final log = <LogEntry>[]; |
56 | | - logForTesting = (message) => log.add(LogEntry('W $message')); |
57 | | - |
58 | | - // Create the watcher and [ClientSimulator]. |
59 | | - final watcher = createWatcher(path: temp.path); |
60 | | - final client = await ClientSimulator.watch( |
61 | | - watcher: watcher, log: (message) => log.add(LogEntry('C $message'))); |
62 | | - addTearDown(client.close); |
63 | | - |
64 | | - // 40 iterations of making changes, waiting for events to settle, and |
65 | | - // checking for consistency. |
66 | | - final changer = FileChanger(temp.path); |
67 | | - for (var i = 0; endlessMode || i != 40; ++i) { |
68 | | - final runSeed = seed ?? i; |
69 | | - log.clear(); |
70 | | - if (endlessMode) stdout.write('.'); |
71 | | - for (final entity in temp.listSync()) { |
72 | | - entity.deleteSync(recursive: true); |
73 | | - } |
74 | | - // File changes. |
75 | | - log.addAll(await changer.changeFiles(times: 200, seed: runSeed)); |
76 | | - |
77 | | - // Give time for events to arrive. To allow tests to run quickly when the |
78 | | - // events are handled quickly, poll and continue if verification passes. |
79 | | - var succeeded = false; |
80 | | - for (var waits = 0; waits != 20; ++waits) { |
81 | | - if (client.verify()) { |
82 | | - succeeded = true; |
83 | | - break; |
84 | | - } |
85 | | - await client.waitForNoEvents(const Duration(milliseconds: 100)); |
86 | | - } |
87 | | - |
88 | | - // Fail the test if still not consistent. |
89 | | - if (!succeeded) { |
90 | | - if (endlessMode) print(''); |
91 | | - client.verify(printOnFailure: printOnFailure); |
92 | | - // Write the file operations before the failure to a log, fail the test. |
93 | | - final logTemp = Directory.systemTemp.createTempSync(); |
94 | | - final logPath = p.join(logTemp.path, 'log.txt'); |
95 | | - |
96 | | - // Sort the log entries by timestamp. |
97 | | - log.sort(); |
98 | 28 |
|
99 | | - File(logPath).writeAsStringSync(log.map((m) => '$m\n').join('')); |
100 | | - fail(''' |
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 | | -'''); |
107 | | - } |
108 | | - } |
109 | | -} |
110 | | - |
111 | | -/// Main method for running the e2e test without `package:test`. |
112 | | -/// |
113 | | -/// Optionally, pass the seed to run with as the only argument. |
114 | | -/// |
115 | | -/// Exits on failure, or runs forever. |
116 | | -Future<void> main(List<String> arguments) async { |
117 | | - final seed = arguments.isNotEmpty ? int.parse(arguments.first) : null; |
118 | | - final teardowns = <void Function()>[]; |
119 | | - try { |
120 | | - await _runTest( |
121 | | - addTearDown: teardowns.add, |
122 | | - createWatcher: ({required String path}) => DirectoryWatcher(path), |
123 | | - fail: (message) { |
124 | | - print(message); |
125 | | - exit(1); |
126 | | - }, |
127 | | - printOnFailure: print, |
128 | | - endlessMode: true, |
129 | | - seed: seed, |
130 | | - ); |
131 | | - } finally { |
132 | | - for (final teardown in teardowns) { |
133 | | - teardown(); |
134 | | - } |
| 29 | + // Specific test cases that have caught bugs. |
| 30 | + for (final testCase in testCases) { |
| 31 | + test('end to end test: ${testCase.name}', |
| 32 | + timeout: const Timeout(Duration(minutes: 5)), () async { |
| 33 | + await runTest(name: testCase.name, replayLog: testCase.log, repeats: 100); |
| 34 | + }, skip: testCase.skipOnLinux && Platform.isLinux); |
135 | 35 | } |
136 | 36 | } |
137 | 37 |
|
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 | | -} |
| 38 | +final testCases = [ |
| 39 | + TestCase( |
| 40 | + 'move over, modify, delete in new directory', |
| 41 | + ''' |
| 42 | +F create,62543,809 |
| 43 | +F wait |
| 44 | +F wait |
| 45 | +F wait |
| 46 | +F wait |
| 47 | +F create directory,a |
| 48 | +F create,a/63090,758 |
| 49 | +F move file over file,62543,a/63090 |
| 50 | +F modify,a/63090,439 |
| 51 | +F delete,a/63090 |
| 52 | +''', |
| 53 | + skipOnLinux: true, |
| 54 | + ) |
| 55 | +]; |
0 commit comments