mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
<!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> Converted more null checks to null aware elements. The analyzer had many false negatives and the regex became pretty wild to find many of them 😂 I've found more in the engine folder but I'll apply the changes in a separate PR Part of #172188 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
393 lines
13 KiB
Dart
393 lines
13 KiB
Dart
// 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:args/command_runner.dart';
|
|
import 'package:collection/collection.dart';
|
|
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/test.dart' as test_package show test;
|
|
import 'package:test/test.dart' hide test;
|
|
import 'package:unified_analytics/unified_analytics.dart';
|
|
|
|
import 'fakes.dart';
|
|
|
|
export 'package:path/path.dart' show Context; // flutter_ignore: package_path_import
|
|
export 'package:test/test.dart' hide isInstanceOf, test;
|
|
|
|
void tryToDelete(FileSystemEntity fileEntity) {
|
|
// 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 (fileEntity.existsSync()) {
|
|
fileEntity.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 ${fileEntity.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;
|
|
case 'data':
|
|
final 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)!);
|
|
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 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].
|
|
///
|
|
/// [message] is matched using the [contains] matcher.
|
|
Matcher throwsToolExit({int? exitCode, Pattern? message}) {
|
|
var result = const TypeMatcher<ToolExit>();
|
|
|
|
if (exitCode != null) {
|
|
result = result.having((ToolExit e) => e.exitCode, 'exitCode', equals(exitCode));
|
|
}
|
|
if (message != null) {
|
|
result = result.having((ToolExit e) => e.message, 'message', contains(message));
|
|
}
|
|
|
|
return throwsA(result);
|
|
}
|
|
|
|
/// Matcher for functions that throw [UsageException].
|
|
Matcher throwsUsageException({Pattern? message}) {
|
|
Matcher matcher = _isUsageException;
|
|
if (message != null) {
|
|
matcher = allOf(matcher, (UsageException e) => e.message.contains(message));
|
|
}
|
|
return throwsA(matcher);
|
|
}
|
|
|
|
/// Matcher for [UsageException]s.
|
|
final TypeMatcher<UsageException> _isUsageException = isA<UsageException>();
|
|
|
|
/// 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) {
|
|
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) {
|
|
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 [globals.fs] or [globals.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:
|
|
///
|
|
/// ```dart
|
|
/// 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 _contextErrors = <String, Map<FileSystemOp, FileSystemException>>{};
|
|
final _tempErrors = <FileSystemOp, FileSystemException>{};
|
|
static final _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;
|
|
}
|
|
}
|
|
|
|
/// This method is required to fetch an instance of [FakeAnalytics]
|
|
/// because there is initialization logic that is required. An initial
|
|
/// instance will first be created and will let package:unified_analytics
|
|
/// know that the consent message has been shown. After confirming on the first
|
|
/// instance, then a second instance will be generated and returned. This second
|
|
/// instance will be cleared to send events.
|
|
FakeAnalytics getInitializedFakeAnalyticsInstance({
|
|
required MemoryFileSystem fs,
|
|
required FakeFlutterVersion fakeFlutterVersion,
|
|
String? clientIde,
|
|
String? enabledFeatures,
|
|
}) {
|
|
final Directory homeDirectory = fs.directory('/');
|
|
final FakeAnalytics initialAnalytics = Analytics.fake(
|
|
tool: DashTool.flutterTool,
|
|
homeDirectory: homeDirectory,
|
|
dartVersion: fakeFlutterVersion.dartSdkVersion,
|
|
fs: fs,
|
|
flutterChannel: fakeFlutterVersion.channel,
|
|
flutterVersion: fakeFlutterVersion.getVersionString(),
|
|
);
|
|
initialAnalytics.clientShowedMessage();
|
|
|
|
return Analytics.fake(
|
|
tool: DashTool.flutterTool,
|
|
homeDirectory: homeDirectory,
|
|
dartVersion: fakeFlutterVersion.dartSdkVersion,
|
|
fs: fs,
|
|
flutterChannel: fakeFlutterVersion.channel,
|
|
flutterVersion: fakeFlutterVersion.getVersionString(),
|
|
clientIde: clientIde,
|
|
enabledFeatures: enabledFeatures,
|
|
);
|
|
}
|
|
|
|
/// Returns "true" if the timing event searched for exists in [sentEvents].
|
|
///
|
|
/// This utility function allows us to check for an instance of
|
|
/// [Event.timing] within a [FakeAnalytics] instance. Normally, we can
|
|
/// use the equality operator for [Event] to check if the event exists, but
|
|
/// we are unable to do so for the timing event because the elapsed time
|
|
/// is variable so we cannot predict what that value will be in tests.
|
|
///
|
|
/// This function allows us to check for the other keys that have
|
|
/// string values by removing the `elapsedMilliseconds` from the
|
|
/// [Event.eventData] map and checking for a match.
|
|
bool analyticsTimingEventExists({
|
|
required List<Event> sentEvents,
|
|
required String workflow,
|
|
required String variableName,
|
|
String? label,
|
|
}) {
|
|
final lookup = <String, String>{
|
|
'workflow': workflow,
|
|
'variableName': variableName,
|
|
'label': ?label,
|
|
};
|
|
|
|
for (final e in sentEvents) {
|
|
final eventData = <String, Object?>{...e.eventData};
|
|
eventData.remove('elapsedMilliseconds');
|
|
|
|
if (const DeepCollectionEquality().equals(lookup, eventData)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|