common.dart 9.87 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:async';

7
import 'package:file/memory.dart';
8 9 10 11
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';
12
import 'package:flutter_tools/src/base/platform.dart';
13
import 'package:flutter_tools/src/globals.dart' as globals;
14
import 'package:meta/meta.dart';
15
import 'package:path/path.dart' as path; // flutter_ignore: package_path_import
16 17 18 19
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
20

21 22 23 24 25
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 {
26 27 28
    if (directory.existsSync()) {
      directory.deleteSync(recursive: true);
    }
29
  } on FileSystemException catch (error) {
30 31 32
    // 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
33 34 35 36
    print('Failed to delete ${directory.path}: $error');
  }
}

37 38 39 40 41 42
/// 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() {
43 44
  const Platform platform = LocalPlatform();
  if (platform.environment.containsKey('FLUTTER_ROOT')) {
45
    return platform.environment['FLUTTER_ROOT']!;
46
  }
47

48
  Error invalidScript() => StateError('Could not determine flutter_tools/ path from script URL (${globals.platform.script}); consider setting FLUTTER_ROOT explicitly.');
49 50

  Uri scriptUri;
51
  switch (platform.script.scheme) {
52
    case 'file':
53
      scriptUri = platform.script;
54 55
      break;
    case 'data':
56
      final RegExp flutterTools = RegExp(r'(file://[^"]*[/\\]flutter_tools[/\\][^"]+\.dart)', multiLine: true);
57
      final Match? match = flutterTools.firstMatch(Uri.decodeFull(platform.script.path));
58
      if (match == null) {
59
        throw invalidScript();
60
      }
61
      scriptUri = Uri.parse(match.group(1)!);
62 63 64 65 66
      break;
    default:
      throw invalidScript();
  }

67
  final List<String> parts = path.split(globals.localFileSystem.path.fromUri(scriptUri));
68
  final int toolsIndex = parts.indexOf('flutter_tools');
69
  if (toolsIndex == -1) {
70
    throw invalidScript();
71
  }
72 73
  final String toolsPath = path.joinAll(parts.sublist(0, toolsIndex + 1));
  return path.normalize(path.join(toolsPath, '..', '..'));
74 75
}

76 77 78 79 80 81 82 83 84 85 86 87
/// 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;
}

Dan Field's avatar
Dan Field committed
88 89 90
/// Matcher for functions that throw [AssertionError].
final Matcher throwsAssertionError = throwsA(isA<AssertionError>());

91
/// Matcher for functions that throw [ToolExit].
92 93
Matcher throwsToolExit({ int? exitCode, Pattern? message }) {
  Matcher matcher = _isToolExit;
94
  if (exitCode != null) {
95
    matcher = allOf(matcher, (ToolExit e) => e.exitCode == exitCode);
96 97
  }
  if (message != null) {
98
    matcher = allOf(matcher, (ToolExit e) => e.message?.contains(message) ?? false);
99
  }
100
  return throwsA(matcher);
101 102 103
}

/// Matcher for [ToolExit]s.
104
final TypeMatcher<ToolExit> _isToolExit = isA<ToolExit>();
105

106
/// Matcher for functions that throw [ProcessException].
107 108
Matcher throwsProcessException({ Pattern? message }) {
  Matcher matcher = _isProcessException;
109
  if (message != null) {
110
    matcher = allOf(matcher, (ProcessException e) => e.message.contains(message));
111 112
  }
  return throwsA(matcher);
113 114
}

115
/// Matcher for [ProcessException]s.
116
final TypeMatcher<ProcessException> _isProcessException = isA<ProcessException>();
117

118 119 120 121 122 123
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);
124 125
  // Catch all exceptions to give a better test failure message.
  } catch (e, trace) { // ignore: avoid_catches_without_on_clauses
126 127 128
    fail('ToolExit expected, got $e\n$trace');
  }
}
129

130 131 132 133 134 135 136 137 138
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');
  }
}

kwkr's avatar
kwkr committed
139 140 141 142 143 144 145 146 147
Matcher containsIgnoringWhitespace(String toSearch) {
  return predicate(
    (String source) {
      return collapseWhitespace(source).contains(collapseWhitespace(toSearch));
    },
    'contains "$toSearch" ignoring whitespace.',
  );
}

148 149 150 151
/// The tool overrides `test` to ensure that files created under the
/// system temporary directory are deleted after each test by calling
/// `LocalFileSystem.dispose()`.
@isTest
152
void test(String description, FutureOr<void> Function() body, {
153
  String? testOn,
154
  dynamic skip,
155 156 157
  List<String>? tags,
  Map<String, dynamic>? onPlatform,
  int? retry,
158 159 160 161 162
}) {
  test_package.test(
    description,
    () async {
      addTearDown(() async {
163
        await globals.localFileSystem.dispose();
164
      });
165

166 167 168 169 170 171 172
      return body();
    },
    skip: skip,
    tags: tags,
    onPlatform: onPlatform,
    retry: retry,
    testOn: testOn,
173 174 175
    // We don't support "timeout"; see ../../dart_test.yaml which
    // configures all tests to have a 15 minute timeout which should
    // definitely be enough.
176 177 178
  );
}

179 180
/// Executes a test body in zone that does not allow context-based injection.
///
181
/// For classes which have been refactored to exclude context-based injection
182 183 184 185 186 187
/// 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
188
void testWithoutContext(String description, FutureOr<void> Function() body, {
189
  String? testOn,
190
  dynamic skip,
191 192 193
  List<String>? tags,
  Map<String, dynamic>? onPlatform,
  int? retry,
194
}) {
195
  return test(
196 197
    description, () async {
      return runZoned(body, zoneValues: <Object, Object>{
198
        contextKey: const _NoContext(),
199 200 201 202 203 204 205
      });
    },
    skip: skip,
    tags: tags,
    onPlatform: onPlatform,
    retry: retry,
    testOn: testOn,
206 207 208
    // We don't support "timeout"; see ../../dart_test.yaml which
    // configures all tests to have a 15 minute timeout which should
    // definitely be enough.
209 210 211 212 213 214 215 216
  );
}

/// 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.
217 218
class _NoContext implements AppContext {
  const _NoContext();
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233

  @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>({
234 235 236 237 238
    required FutureOr<V> Function() body,
    String? name,
    Map<Type, Generator>? overrides,
    Map<Type, Generator>? fallbacks,
    ZoneSpecification? zoneSpecification,
239 240 241 242
  }) async {
    return body();
  }
}
243

244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
/// 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>>{};
262 263
  final Map<FileSystemOp, FileSystemException> _tempErrors = <FileSystemOp, FileSystemException>{};
  static final RegExp _tempDirectoryEnd = RegExp('rand[0-9]+');
264 265 266 267 268 269

  /// 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>{};
270
    _contextErrors[path]![operation] = exception;
271 272
  }

273 274 275 276 277
  void addTempError(FileSystemOp operation, FileSystemException exception) {
    _tempErrors[operation] = exception;
  }

  /// Tear-off this method and pass it to the memory filesystem `opHandle` parameter.
278
  void opHandle(String path, FileSystemOp operation) {
279 280 281 282 283 284
    if (path.startsWith('.tmp_') || _tempDirectoryEnd.firstMatch(path) != null) {
      final FileSystemException? exception = _tempErrors[operation];
      if (exception != null) {
        throw exception;
      }
    }
285
    final Map<FileSystemOp, FileSystemException>? exceptions = _contextErrors[path];
286 287 288
    if (exceptions == null) {
      return;
    }
289
    final FileSystemException? exception = exceptions[operation];
290 291 292 293 294 295
    if (exception == null) {
      return;
    }
    throw exception;
  }
}