| // 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; |
| } |
| } |