1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
// Copyright 2014 The Flutter Authors. 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 'package:file/memory.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path; // flutter_ignore: package_path_import
import 'package:test_api/test_api.dart' as test_package show test; // ignore: deprecated_member_use
import 'package:test_api/test_api.dart' hide test; // ignore: deprecated_member_use
export 'package:test_api/test_api.dart' hide test, isInstanceOf; // ignore: deprecated_member_use
void tryToDelete(Directory directory) {
// This should not be necessary, but it turns out that
// on Windows it's common for deletions to fail due to
// bogus (we think) "access denied" errors.
try {
if (directory.existsSync()) {
directory.deleteSync(recursive: true);
}
} on FileSystemException catch (error) {
// We print this so that it's visible in the logs, to get an idea of how
// common this problem is, and if any patterns are ever noticed by anyone.
// ignore: avoid_print
print('Failed to delete ${directory.path}: $error');
}
}
/// Gets the path to the root of the Flutter repository.
///
/// This will first look for a `FLUTTER_ROOT` environment variable. If the
/// environment variable is set, it will be returned. Otherwise, this will
/// deduce the path from `platform.script`.
String getFlutterRoot() {
const Platform platform = LocalPlatform();
if (platform.environment.containsKey('FLUTTER_ROOT')) {
return platform.environment['FLUTTER_ROOT']!;
}
Error invalidScript() => StateError('Could not determine flutter_tools/ path from script URL (${globals.platform.script}); consider setting FLUTTER_ROOT explicitly.');
Uri scriptUri;
switch (platform.script.scheme) {
case 'file':
scriptUri = platform.script;
break;
case 'data':
final RegExp flutterTools = RegExp(r'(file://[^"]*[/\\]flutter_tools[/\\][^"]+\.dart)', multiLine: true);
final Match? match = flutterTools.firstMatch(Uri.decodeFull(platform.script.path));
if (match == null) {
throw invalidScript();
}
scriptUri = Uri.parse(match.group(1)!);
break;
default:
throw invalidScript();
}
final List<String> parts = path.split(globals.localFileSystem.path.fromUri(scriptUri));
final int toolsIndex = parts.indexOf('flutter_tools');
if (toolsIndex == -1) {
throw invalidScript();
}
final String toolsPath = path.joinAll(parts.sublist(0, toolsIndex + 1));
return path.normalize(path.join(toolsPath, '..', '..'));
}
/// Capture console print events into a string buffer.
Future<StringBuffer> capturedConsolePrint(Future<void> Function() body) async {
final StringBuffer buffer = StringBuffer();
await runZoned<Future<void>>(() async {
// Service the event loop.
await body();
}, zoneSpecification: ZoneSpecification(print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
buffer.writeln(line);
}));
return buffer;
}
/// Matcher for functions that throw [AssertionError].
final Matcher throwsAssertionError = throwsA(isA<AssertionError>());
/// Matcher for functions that throw [ToolExit].
Matcher throwsToolExit({ int? exitCode, Pattern? message }) {
Matcher matcher = _isToolExit;
if (exitCode != null) {
matcher = allOf(matcher, (ToolExit e) => e.exitCode == exitCode);
}
if (message != null) {
matcher = allOf(matcher, (ToolExit e) => e.message?.contains(message) ?? false);
}
return throwsA(matcher);
}
/// Matcher for [ToolExit]s.
final TypeMatcher<ToolExit> _isToolExit = isA<ToolExit>();
/// Matcher for functions that throw [ProcessException].
Matcher throwsProcessException({ Pattern? message }) {
Matcher matcher = _isProcessException;
if (message != null) {
matcher = allOf(matcher, (ProcessException e) => e.message.contains(message));
}
return throwsA(matcher);
}
/// Matcher for [ProcessException]s.
final TypeMatcher<ProcessException> _isProcessException = isA<ProcessException>();
Future<void> expectToolExitLater(Future<dynamic> future, Matcher messageMatcher) async {
try {
await future;
fail('ToolExit expected, but nothing thrown');
} on ToolExit catch(e) {
expect(e.message, messageMatcher);
// Catch all exceptions to give a better test failure message.
} catch (e, trace) { // ignore: avoid_catches_without_on_clauses
fail('ToolExit expected, got $e\n$trace');
}
}
Future<void> expectReturnsNormallyLater(Future<dynamic> future) async {
try {
await future;
// Catch all exceptions to give a better test failure message.
} catch (e, trace) { // ignore: avoid_catches_without_on_clauses
fail('Expected to run with no exceptions, got $e\n$trace');
}
}
Matcher containsIgnoringWhitespace(String toSearch) {
return predicate(
(String source) {
return collapseWhitespace(source).contains(collapseWhitespace(toSearch));
},
'contains "$toSearch" ignoring whitespace.',
);
}
/// The tool overrides `test` to ensure that files created under the
/// system temporary directory are deleted after each test by calling
/// `LocalFileSystem.dispose()`.
@isTest
void test(String description, FutureOr<void> Function() body, {
String? testOn,
dynamic skip,
List<String>? tags,
Map<String, dynamic>? onPlatform,
int? retry,
}) {
test_package.test(
description,
() async {
addTearDown(() async {
await globals.localFileSystem.dispose();
});
return body();
},
skip: skip,
tags: tags,
onPlatform: onPlatform,
retry: retry,
testOn: testOn,
// We don't support "timeout"; see ../../dart_test.yaml which
// configures all tests to have a 15 minute timeout which should
// definitely be enough.
);
}
/// Executes a test body in zone that does not allow context-based injection.
///
/// For classes which have been refactored to exclude context-based injection
/// or globals like [fs] or [platform], prefer using this test method as it
/// will prevent accidentally including these context getters in future code
/// changes.
///
/// For more information, see https://github.com/flutter/flutter/issues/47161
@isTest
void testWithoutContext(String description, FutureOr<void> Function() body, {
String? testOn,
dynamic skip,
List<String>? tags,
Map<String, dynamic>? onPlatform,
int? retry,
}) {
return test(
description, () async {
return runZoned(body, zoneValues: <Object, Object>{
contextKey: const _NoContext(),
});
},
skip: skip,
tags: tags,
onPlatform: onPlatform,
retry: retry,
testOn: testOn,
// We don't support "timeout"; see ../../dart_test.yaml which
// configures all tests to have a 15 minute timeout which should
// definitely be enough.
);
}
/// An implementation of [AppContext] that throws if context.get is called in the test.
///
/// The intention of the class is to ensure we do not accidentally regress when
/// moving towards more explicit dependency injection by accidentally using
/// a Zone value in place of a constructor parameter.
class _NoContext implements AppContext {
const _NoContext();
@override
T get<T>() {
throw UnsupportedError(
'context.get<$T> is not supported in test methods. '
'Use Testbed or testUsingContext if accessing Zone injected '
'values.'
);
}
@override
String get name => 'No Context';
@override
Future<V> run<V>({
required FutureOr<V> Function() body,
String? name,
Map<Type, Generator>? overrides,
Map<Type, Generator>? fallbacks,
ZoneSpecification? zoneSpecification,
}) async {
return body();
}
}
/// Allows inserting file system exceptions into certain
/// [MemoryFileSystem] operations by tagging path/op combinations.
///
/// Example use:
///
/// ```
/// void main() {
/// var handler = FileExceptionHandler();
/// var fs = MemoryFileSystem(opHandle: handler.opHandle);
///
/// var file = fs.file('foo')..createSync();
/// handler.addError(file, FileSystemOp.read, FileSystemException('Error Reading foo'));
///
/// expect(() => file.writeAsStringSync('A'), throwsA(isA<FileSystemException>()));
/// }
/// ```
class FileExceptionHandler {
final Map<String, Map<FileSystemOp, FileSystemException>> _contextErrors = <String, Map<FileSystemOp, FileSystemException>>{};
final Map<FileSystemOp, FileSystemException> _tempErrors = <FileSystemOp, FileSystemException>{};
static final RegExp _tempDirectoryEnd = RegExp('rand[0-9]+');
/// Add an exception that will be thrown whenever the file system attached to this
/// handler performs the [operation] on the [entity].
void addError(FileSystemEntity entity, FileSystemOp operation, FileSystemException exception) {
final String path = entity.path;
_contextErrors[path] ??= <FileSystemOp, FileSystemException>{};
_contextErrors[path]![operation] = exception;
}
void addTempError(FileSystemOp operation, FileSystemException exception) {
_tempErrors[operation] = exception;
}
/// Tear-off this method and pass it to the memory filesystem `opHandle` parameter.
void opHandle(String path, FileSystemOp operation) {
if (path.startsWith('.tmp_') || _tempDirectoryEnd.firstMatch(path) != null) {
final FileSystemException? exception = _tempErrors[operation];
if (exception != null) {
throw exception;
}
}
final Map<FileSystemOp, FileSystemException>? exceptions = _contextErrors[path];
if (exceptions == null) {
return;
}
final FileSystemException? exception = exceptions[operation];
if (exception == null) {
return;
}
throw exception;
}
}