| // 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:flutter_tools/src/android/android_workflow.dart'; |
| import 'package:flutter_tools/src/base/bot_detector.dart'; |
| import 'package:flutter_tools/src/base/config.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/logger.dart'; |
| import 'package:flutter_tools/src/base/os.dart'; |
| import 'package:flutter_tools/src/base/process.dart'; |
| import 'package:flutter_tools/src/base/signals.dart'; |
| import 'package:flutter_tools/src/base/template.dart'; |
| import 'package:flutter_tools/src/base/terminal.dart'; |
| import 'package:flutter_tools/src/base/version.dart'; |
| import 'package:flutter_tools/src/cache.dart'; |
| import 'package:flutter_tools/src/context_runner.dart'; |
| import 'package:flutter_tools/src/dart/pub.dart'; |
| import 'package:flutter_tools/src/device.dart'; |
| import 'package:flutter_tools/src/doctor.dart'; |
| import 'package:flutter_tools/src/doctor_validator.dart'; |
| import 'package:flutter_tools/src/globals.dart' as globals; |
| import 'package:flutter_tools/src/ios/plist_parser.dart'; |
| import 'package:flutter_tools/src/ios/simulators.dart'; |
| import 'package:flutter_tools/src/ios/xcodeproj.dart'; |
| import 'package:flutter_tools/src/isolated/mustache_template.dart'; |
| import 'package:flutter_tools/src/persistent_tool_state.dart'; |
| import 'package:flutter_tools/src/project.dart'; |
| import 'package:flutter_tools/src/reporting/crash_reporting.dart'; |
| import 'package:flutter_tools/src/reporting/reporting.dart'; |
| import 'package:flutter_tools/src/version.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:test/fake.dart'; |
| |
| import 'common.dart'; |
| import 'fake_http_client.dart'; |
| import 'fake_process_manager.dart'; |
| import 'fakes.dart'; |
| import 'throwing_pub.dart'; |
| |
| export 'package:flutter_tools/src/base/context.dart' show Generator; |
| |
| export 'fake_process_manager.dart' show FakeCommand, FakeProcessManager, ProcessManager; |
| |
| /// Return the test logger. This assumes that the current Logger is a BufferLogger. |
| BufferLogger get testLogger => context.get<Logger>()! as BufferLogger; |
| |
| FakeDeviceManager get testDeviceManager => context.get<DeviceManager>()! as FakeDeviceManager; |
| |
| @isTest |
| void testUsingContext( |
| String description, |
| dynamic Function() testMethod, { |
| Map<Type, Generator> overrides = const <Type, Generator>{}, |
| bool initializeFlutterRoot = true, |
| String? testOn, |
| bool? skip, // should default to `false`, but https://github.com/dart-lang/test/issues/545 doesn't allow this |
| }) { |
| if (overrides[FileSystem] != null && overrides[ProcessManager] == null) { |
| throw StateError( |
| 'If you override the FileSystem context you must also provide a ProcessManager, ' |
| 'otherwise the processes you launch will not be dealing with the same file system ' |
| 'that you are dealing with in your test.' |
| ); |
| } |
| if (overrides.containsKey(ProcessUtils)) { |
| throw StateError('Do not inject ProcessUtils for testing, use ProcessManager instead.'); |
| } |
| |
| // Ensure we don't rely on the default [Config] constructor which will |
| // leak a sticky $HOME/.flutter_settings behind! |
| Directory? configDir; |
| tearDown(() { |
| if (configDir != null) { |
| tryToDelete(configDir!); |
| configDir = null; |
| } |
| }); |
| Config buildConfig(FileSystem fs) { |
| configDir ??= globals.fs.systemTempDirectory.createTempSync('flutter_config_dir_test.'); |
| return Config.test( |
| name: Config.kFlutterSettings, |
| directory: configDir, |
| logger: globals.logger, |
| ); |
| } |
| PersistentToolState buildPersistentToolState(FileSystem fs) { |
| configDir ??= globals.fs.systemTempDirectory.createTempSync('flutter_config_dir_test.'); |
| return PersistentToolState.test( |
| directory: configDir!, |
| logger: globals.logger, |
| ); |
| } |
| |
| test(description, () async { |
| await runInContext<dynamic>(() { |
| return context.run<dynamic>( |
| name: 'mocks', |
| overrides: <Type, Generator>{ |
| AnsiTerminal: () => AnsiTerminal(platform: globals.platform, stdio: globals.stdio), |
| Config: () => buildConfig(globals.fs), |
| DeviceManager: () => FakeDeviceManager(), |
| Doctor: () => FakeDoctor(globals.logger), |
| FlutterVersion: () => FakeFlutterVersion(), |
| HttpClient: () => FakeHttpClient.any(), |
| IOSSimulatorUtils: () => const NoopIOSSimulatorUtils(), |
| OutputPreferences: () => OutputPreferences.test(), |
| Logger: () => BufferLogger.test(), |
| OperatingSystemUtils: () => FakeOperatingSystemUtils(), |
| PersistentToolState: () => buildPersistentToolState(globals.fs), |
| Usage: () => TestUsage(), |
| XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(), |
| FileSystem: () => LocalFileSystemBlockingSetCurrentDirectory(), |
| PlistParser: () => FakePlistParser(), |
| Signals: () => FakeSignals(), |
| Pub: () => ThrowingPub(), // prevent accidentally using pub. |
| CrashReporter: () => const NoopCrashReporter(), |
| TemplateRenderer: () => const MustacheTemplateRenderer(), |
| }, |
| body: () { |
| return runZonedGuarded<Future<dynamic>>(() { |
| try { |
| return context.run<dynamic>( |
| // Apply the overrides to the test context in the zone since their |
| // instantiation may reference items already stored on the context. |
| overrides: overrides, |
| name: 'test-specific overrides', |
| body: () async { |
| if (initializeFlutterRoot) { |
| // Provide a sane default for the flutterRoot directory. Individual |
| // tests can override this either in the test or during setup. |
| Cache.flutterRoot ??= getFlutterRoot(); |
| } |
| return await testMethod(); |
| }, |
| ); |
| // This catch rethrows, so doesn't need to catch only Exception. |
| } catch (error) { // ignore: avoid_catches_without_on_clauses |
| _printBufferedErrors(context); |
| rethrow; |
| } |
| }, (Object error, StackTrace stackTrace) { |
| // When things fail, it's ok to print to the console! |
| print(error); // ignore: avoid_print |
| print(stackTrace); // ignore: avoid_print |
| _printBufferedErrors(context); |
| throw error; //ignore: only_throw_errors |
| }); |
| }, |
| ); |
| }, overrides: <Type, Generator>{ |
| // This has to go here so that runInContext will pick it up when it tries |
| // to do bot detection before running the closure. This is important |
| // because the test may be giving us a fake HttpClientFactory, which may |
| // throw in unexpected/abnormal ways. |
| // If a test needs a BotDetector that does not always return true, it |
| // can provide the AlwaysFalseBotDetector in the overrides, or its own |
| // BotDetector implementation in the overrides. |
| BotDetector: overrides[BotDetector] ?? () => const FakeBotDetector(true), |
| }); |
| }, testOn: testOn, skip: skip); |
| // We don't support "timeout"; see ../../dart_test.yaml which |
| // configures all tests to have a 15 minute timeout which should |
| // definitely be enough. |
| } |
| |
| void _printBufferedErrors(AppContext testContext) { |
| if (testContext.get<Logger>() is BufferLogger) { |
| final BufferLogger bufferLogger = testContext.get<Logger>()! as BufferLogger; |
| if (bufferLogger.errorText.isNotEmpty) { |
| // This is where the logger outputting errors is implemented, so it has |
| // to use `print`. |
| print(bufferLogger.errorText); // ignore: avoid_print |
| } |
| bufferLogger.clear(); |
| } |
| } |
| |
| class FakeDeviceManager implements DeviceManager { |
| List<Device> attachedDevices = <Device>[]; |
| List<Device> wirelessDevices = <Device>[]; |
| |
| String? _specifiedDeviceId; |
| |
| @override |
| String? get specifiedDeviceId { |
| if (_specifiedDeviceId == null || _specifiedDeviceId == 'all') { |
| return null; |
| } |
| return _specifiedDeviceId; |
| } |
| |
| @override |
| set specifiedDeviceId(String? id) { |
| _specifiedDeviceId = id; |
| } |
| |
| @override |
| bool get hasSpecifiedDeviceId => specifiedDeviceId != null; |
| |
| @override |
| bool get hasSpecifiedAllDevices { |
| return _specifiedDeviceId != null && _specifiedDeviceId == 'all'; |
| } |
| |
| @override |
| Future<List<Device>> getAllDevices({ |
| DeviceDiscoveryFilter? filter, |
| }) async => filteredDevices(filter); |
| |
| @override |
| Future<List<Device>> refreshAllDevices({ |
| Duration? timeout, |
| DeviceDiscoveryFilter? filter, |
| }) async => filteredDevices(filter); |
| |
| @override |
| Future<List<Device>> refreshExtendedWirelessDeviceDiscoverers({ |
| Duration? timeout, |
| DeviceDiscoveryFilter? filter, |
| }) async => filteredDevices(filter); |
| |
| @override |
| Future<List<Device>> getDevicesById( |
| String deviceId, { |
| DeviceDiscoveryFilter? filter, |
| bool waitForDeviceToConnect = false, |
| }) async { |
| return filteredDevices(filter).where((Device device) { |
| return device.id == deviceId || device.id.startsWith(deviceId); |
| }).toList(); |
| } |
| |
| @override |
| Future<List<Device>> getDevices({ |
| DeviceDiscoveryFilter? filter, |
| bool waitForDeviceToConnect = false, |
| }) { |
| return hasSpecifiedDeviceId |
| ? getDevicesById(specifiedDeviceId!, filter: filter) |
| : getAllDevices(filter: filter); |
| } |
| |
| void addAttachedDevice(Device device) => attachedDevices.add(device); |
| void addWirelessDevice(Device device) => wirelessDevices.add(device); |
| |
| @override |
| bool get canListAnything => true; |
| |
| @override |
| Future<List<String>> getDeviceDiagnostics() async => <String>[]; |
| |
| @override |
| List<DeviceDiscovery> get deviceDiscoverers => <DeviceDiscovery>[]; |
| |
| @override |
| DeviceDiscoverySupportFilter deviceSupportFilter({ |
| bool includeDevicesUnsupportedByProject = false, |
| FlutterProject? flutterProject, |
| }) { |
| return TestDeviceDiscoverySupportFilter(); |
| } |
| |
| @override |
| Device? getSingleEphemeralDevice(List<Device> devices) => null; |
| |
| List<Device> filteredDevices(DeviceDiscoveryFilter? filter) { |
| if (filter?.deviceConnectionInterface == DeviceConnectionInterface.attached) { |
| return attachedDevices; |
| } |
| if (filter?.deviceConnectionInterface == DeviceConnectionInterface.wireless) { |
| return wirelessDevices; |
| } |
| return attachedDevices + wirelessDevices; |
| } |
| } |
| |
| class TestDeviceDiscoverySupportFilter extends Fake implements DeviceDiscoverySupportFilter { |
| TestDeviceDiscoverySupportFilter(); |
| } |
| |
| class FakeAndroidLicenseValidator extends Fake implements AndroidLicenseValidator { |
| @override |
| Future<LicensesAccepted> get licensesAccepted async => LicensesAccepted.all; |
| } |
| |
| class FakeDoctor extends Doctor { |
| FakeDoctor(Logger logger) : super(logger: logger); |
| |
| // True for testing. |
| @override |
| bool get canListAnything => true; |
| |
| // True for testing. |
| @override |
| bool get canLaunchAnything => true; |
| |
| @override |
| /// Replaces the android workflow with a version that overrides licensesAccepted, |
| /// to prevent individual tests from having to mock out the process for |
| /// the Doctor. |
| List<DoctorValidator> get validators { |
| final List<DoctorValidator> superValidators = super.validators; |
| return superValidators.map<DoctorValidator>((DoctorValidator v) { |
| if (v is AndroidLicenseValidator) { |
| return FakeAndroidLicenseValidator(); |
| } |
| return v; |
| }).toList(); |
| } |
| } |
| |
| class NoopIOSSimulatorUtils implements IOSSimulatorUtils { |
| const NoopIOSSimulatorUtils(); |
| |
| @override |
| Future<List<IOSSimulator>> getAttachedDevices() async => <IOSSimulator>[]; |
| |
| @override |
| Future<List<IOSSimulatorRuntime>> getAvailableIOSRuntimes() async => <IOSSimulatorRuntime>[]; |
| } |
| |
| class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter { |
| @override |
| bool get isInstalled => true; |
| |
| @override |
| String get versionText => 'Xcode 14'; |
| |
| @override |
| Version get version => Version(14, null, null); |
| |
| @override |
| String get build => '14A309'; |
| |
| @override |
| Future<Map<String, String>> getBuildSettings( |
| String projectPath, { |
| XcodeProjectBuildContext? buildContext, |
| Duration timeout = const Duration(minutes: 1), |
| }) async { |
| return <String, String>{}; |
| } |
| |
| @override |
| Future<String> pluginsBuildSettingsOutput( |
| Directory podXcodeProject, { |
| Duration timeout = const Duration(minutes: 1), |
| }) async { |
| return ''; |
| } |
| |
| @override |
| Future<void> cleanWorkspace(String workspacePath, String scheme, { bool verbose = false }) async { } |
| |
| @override |
| Future<XcodeProjectInfo> getInfo(String projectPath, {String? projectFilename}) async { |
| return XcodeProjectInfo( |
| <String>['Runner'], |
| <String>['Debug', 'Release'], |
| <String>['Runner'], |
| BufferLogger.test(), |
| ); |
| } |
| |
| @override |
| List<String> xcrunCommand() => <String>['xcrun']; |
| } |
| |
| /// Prevent test crashest from being reported to the crash backend. |
| class NoopCrashReporter implements CrashReporter { |
| const NoopCrashReporter(); |
| |
| @override |
| Future<void> informUser(CrashDetails details, File crashFile) async { } |
| } |
| |
| class LocalFileSystemBlockingSetCurrentDirectory extends LocalFileSystem { |
| LocalFileSystemBlockingSetCurrentDirectory() : super.test( |
| signals: LocalSignals.instance, |
| ); |
| |
| @override |
| set currentDirectory(dynamic value) { |
| throw Exception('globals.fs.currentDirectory should not be set on the local file system during ' |
| 'tests as this can cause race conditions with concurrent tests. ' |
| 'Consider using a MemoryFileSystem for testing if possible or refactor ' |
| 'code to not require setting globals.fs.currentDirectory.'); |
| } |
| } |
| |
| class FakeSignals implements Signals { |
| @override |
| Object addHandler(ProcessSignal signal, SignalHandler handler) { |
| return const Object(); |
| } |
| |
| @override |
| Future<bool> removeHandler(ProcessSignal signal, Object token) async { |
| return true; |
| } |
| |
| @override |
| Stream<Object> get errors => const Stream<Object>.empty(); |
| } |