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

import 'dart:async';

7
import 'package:flutter_tools/src/android/android_workflow.dart';
Dan Field's avatar
Dan Field committed
8
import 'package:flutter_tools/src/base/bot_detector.dart';
9
import 'package:flutter_tools/src/base/config.dart';
10
import 'package:flutter_tools/src/base/context.dart';
11
import 'package:flutter_tools/src/base/file_system.dart';
12
import 'package:flutter_tools/src/base/io.dart';
13
import 'package:flutter_tools/src/base/logger.dart';
14
import 'package:flutter_tools/src/base/os.dart';
Dan Field's avatar
Dan Field committed
15
import 'package:flutter_tools/src/base/process.dart';
16
import 'package:flutter_tools/src/base/signals.dart';
17
import 'package:flutter_tools/src/base/template.dart';
18
import 'package:flutter_tools/src/base/terminal.dart';
19
import 'package:flutter_tools/src/base/version.dart';
20
import 'package:flutter_tools/src/cache.dart';
21
import 'package:flutter_tools/src/context_runner.dart';
22
import 'package:flutter_tools/src/dart/pub.dart';
23
import 'package:flutter_tools/src/device.dart';
24
import 'package:flutter_tools/src/doctor.dart';
25
import 'package:flutter_tools/src/doctor_validator.dart';
26
import 'package:flutter_tools/src/globals.dart' as globals;
27
import 'package:flutter_tools/src/ios/plist_parser.dart';
28
import 'package:flutter_tools/src/ios/simulators.dart';
29
import 'package:flutter_tools/src/ios/xcodeproj.dart';
30
import 'package:flutter_tools/src/isolated/mustache_template.dart';
31
import 'package:flutter_tools/src/persistent_tool_state.dart';
32
import 'package:flutter_tools/src/project.dart';
33
import 'package:flutter_tools/src/reporting/crash_reporting.dart';
34
import 'package:flutter_tools/src/reporting/reporting.dart';
35
import 'package:flutter_tools/src/version.dart';
36
import 'package:meta/meta.dart';
37
import 'package:test/fake.dart';
38

39
import 'common.dart';
40
import 'fake_http_client.dart';
41
import 'fake_process_manager.dart';
42
import 'fakes.dart';
43
import 'throwing_pub.dart';
44

45
export 'package:flutter_tools/src/base/context.dart' show Generator;
46

47
export 'fake_process_manager.dart' show FakeCommand, FakeProcessManager, ProcessManager;
48

49
/// Return the test logger. This assumes that the current Logger is a BufferLogger.
50
BufferLogger get testLogger => context.get<Logger>()! as BufferLogger;
51

52
FakeDeviceManager get testDeviceManager => context.get<DeviceManager>()! as FakeDeviceManager;
53

54
@isTest
55 56
void testUsingContext(
  String description,
57
  dynamic Function() testMethod, {
58 59
  Map<Type, Generator> overrides = const <Type, Generator>{},
  bool initializeFlutterRoot = true,
60 61
  String? testOn,
  bool? skip, // should default to `false`, but https://github.com/dart-lang/test/issues/545 doesn't allow this
62
}) {
63 64 65 66 67 68 69
  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.'
    );
  }
Dan Field's avatar
Dan Field committed
70 71 72
  if (overrides.containsKey(ProcessUtils)) {
    throw StateError('Do not inject ProcessUtils for testing, use ProcessManager instead.');
  }
73

74 75
  // Ensure we don't rely on the default [Config] constructor which will
  // leak a sticky $HOME/.flutter_settings behind!
76
  Directory? configDir;
77
  tearDown(() {
78
    if (configDir != null) {
79
      tryToDelete(configDir!);
80 81
      configDir = null;
    }
82 83
  });
  Config buildConfig(FileSystem fs) {
84
    configDir ??= globals.fs.systemTempDirectory.createTempSync('flutter_config_dir_test.');
85
    return Config.test(
86
      name: Config.kFlutterSettings,
87 88
      directory: configDir,
      logger: globals.logger,
89
    );
90
  }
91
  PersistentToolState buildPersistentToolState(FileSystem fs) {
92
    configDir ??= globals.fs.systemTempDirectory.createTempSync('flutter_config_dir_test.');
93
    return PersistentToolState.test(
94
      directory: configDir!,
95 96
      logger: globals.logger,
    );
97
  }
98

99
  test(description, () async {
100
    await runInContext<dynamic>(() {
101
      return context.run<dynamic>(
102 103
        name: 'mocks',
        overrides: <Type, Generator>{
104
          AnsiTerminal: () => AnsiTerminal(platform: globals.platform, stdio: globals.stdio),
105
          Config: () => buildConfig(globals.fs),
106
          DeviceManager: () => FakeDeviceManager(),
107
          Doctor: () => FakeDoctor(globals.logger),
108
          FlutterVersion: () => FakeFlutterVersion(),
109
          HttpClient: () => FakeHttpClient.any(),
110
          IOSSimulatorUtils: () => const NoopIOSSimulatorUtils(),
111
          OutputPreferences: () => OutputPreferences.test(),
112
          Logger: () => BufferLogger.test(),
113
          OperatingSystemUtils: () => FakeOperatingSystemUtils(),
114
          PersistentToolState: () => buildPersistentToolState(globals.fs),
115
          Usage: () => TestUsage(),
116
          XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(),
117
          FileSystem: () => LocalFileSystemBlockingSetCurrentDirectory(),
118
          PlistParser: () => FakePlistParser(),
119
          Signals: () => FakeSignals(),
120
          Pub: () => ThrowingPub(), // prevent accidentally using pub.
121
          CrashReporter: () => const NoopCrashReporter(),
122
          TemplateRenderer: () => const MustacheTemplateRenderer(),
123 124
        },
        body: () {
125
          return runZonedGuarded<Future<dynamic>>(() {
126
            try {
127
              return context.run<dynamic>(
128 129 130 131 132
                // 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 {
133 134 135
                  if (initializeFlutterRoot) {
                    // Provide a sane default for the flutterRoot directory. Individual
                    // tests can override this either in the test or during setup.
136
                    Cache.flutterRoot ??= getFlutterRoot();
137
                  }
138 139 140
                  return await testMethod();
                },
              );
141 142
            // This catch rethrows, so doesn't need to catch only Exception.
            } catch (error) { // ignore: avoid_catches_without_on_clauses
143 144 145
              _printBufferedErrors(context);
              rethrow;
            }
146 147 148 149
          }, (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
150
            _printBufferedErrors(context);
151
            throw error; //ignore: only_throw_errors
152 153 154
          });
        },
      );
Dan Field's avatar
Dan Field committed
155 156
    }, overrides: <Type, Generator>{
      // This has to go here so that runInContext will pick it up when it tries
157
      // to do bot detection before running the closure. This is important
Dan Field's avatar
Dan Field committed
158 159 160 161 162
      // 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.
163
      BotDetector: overrides[BotDetector] ?? () => const FakeBotDetector(true),
164
    });
165 166 167 168
  }, 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.
169 170
}

171
void _printBufferedErrors(AppContext testContext) {
172
  if (testContext.get<Logger>() is BufferLogger) {
173
    final BufferLogger bufferLogger = testContext.get<Logger>()! as BufferLogger;
174
    if (bufferLogger.errorText.isNotEmpty) {
175 176 177
      // This is where the logger outputting errors is implemented, so it has
      // to use `print`.
      print(bufferLogger.errorText); // ignore: avoid_print
178
    }
179 180 181 182
    bufferLogger.clear();
  }
}

183
class FakeDeviceManager implements DeviceManager {
184 185
  List<Device> attachedDevices = <Device>[];
  List<Device> wirelessDevices = <Device>[];
186

187
  String? _specifiedDeviceId;
188 189

  @override
190
  String? get specifiedDeviceId {
191
    if (_specifiedDeviceId == null || _specifiedDeviceId == 'all') {
192
      return null;
193
    }
194 195 196
    return _specifiedDeviceId;
  }

197
  @override
198
  set specifiedDeviceId(String? id) {
199 200
    _specifiedDeviceId = id;
  }
201 202

  @override
203 204
  bool get hasSpecifiedDeviceId => specifiedDeviceId != null;

205 206 207 208 209
  @override
  bool get hasSpecifiedAllDevices {
    return _specifiedDeviceId != null && _specifiedDeviceId == 'all';
  }

210
  @override
211 212
  Future<List<Device>> getAllDevices({
    DeviceDiscoveryFilter? filter,
213
  }) async => filteredDevices(filter);
214

215
  @override
216 217 218
  Future<List<Device>> refreshAllDevices({
    Duration? timeout,
    DeviceDiscoveryFilter? filter,
219
  }) async => filteredDevices(filter);
220

221 222 223 224 225 226
  @override
  Future<List<Device>> refreshExtendedWirelessDeviceDiscoverers({
    Duration? timeout,
    DeviceDiscoveryFilter? filter,
  }) async => filteredDevices(filter);

227
  @override
228 229 230
  Future<List<Device>> getDevicesById(
    String deviceId, {
    DeviceDiscoveryFilter? filter,
231
    bool waitForDeviceToConnect = false,
232
  }) async {
233
    return filteredDevices(filter).where((Device device) {
234 235
      return device.id == deviceId || device.id.startsWith(deviceId);
    }).toList();
236 237
  }

238
  @override
239 240
  Future<List<Device>> getDevices({
    DeviceDiscoveryFilter? filter,
241
    bool waitForDeviceToConnect = false,
242
  }) {
243
    return hasSpecifiedDeviceId
244 245
        ? getDevicesById(specifiedDeviceId!, filter: filter)
        : getAllDevices(filter: filter);
246 247
  }

248 249
  void addAttachedDevice(Device device) => attachedDevices.add(device);
  void addWirelessDevice(Device device) => wirelessDevices.add(device);
250 251 252 253 254 255

  @override
  bool get canListAnything => true;

  @override
  Future<List<String>> getDeviceDiagnostics() async => <String>[];
256 257 258

  @override
  List<DeviceDiscovery> get deviceDiscoverers => <DeviceDiscovery>[];
259 260

  @override
261 262 263 264 265
  DeviceDiscoverySupportFilter deviceSupportFilter({
    bool includeDevicesUnsupportedByProject = false,
    FlutterProject? flutterProject,
  }) {
    return TestDeviceDiscoverySupportFilter();
266
  }
267 268 269

  @override
  Device? getSingleEphemeralDevice(List<Device> devices) => null;
270 271 272 273 274 275 276 277 278 279

  List<Device> filteredDevices(DeviceDiscoveryFilter? filter) {
    if (filter?.deviceConnectionInterface == DeviceConnectionInterface.attached) {
      return attachedDevices;
    }
    if (filter?.deviceConnectionInterface == DeviceConnectionInterface.wireless) {
      return wirelessDevices;
    }
    return attachedDevices + wirelessDevices;
  }
280 281
}

282 283 284 285
class TestDeviceDiscoverySupportFilter extends Fake implements DeviceDiscoverySupportFilter {
  TestDeviceDiscoverySupportFilter();
}

286
class FakeAndroidLicenseValidator extends Fake implements AndroidLicenseValidator {
287 288 289 290
  @override
  Future<LicensesAccepted> get licensesAccepted async => LicensesAccepted.all;
}

291
class FakeDoctor extends Doctor {
292 293
  FakeDoctor(Logger logger) : super(logger: logger);

294 295 296 297
  // True for testing.
  @override
  bool get canListAnything => true;

298
  // True for testing.
299
  @override
300
  bool get canLaunchAnything => true;
301 302 303 304 305 306 307

  @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;
308
    return superValidators.map<DoctorValidator>((DoctorValidator v) {
309
      if (v is AndroidLicenseValidator) {
310
        return FakeAndroidLicenseValidator();
311 312 313 314
      }
      return v;
    }).toList();
  }
315
}
316

317 318
class NoopIOSSimulatorUtils implements IOSSimulatorUtils {
  const NoopIOSSimulatorUtils();
319

320 321
  @override
  Future<List<IOSSimulator>> getAttachedDevices() async => <IOSSimulator>[];
322 323 324

  @override
  Future<List<IOSSimulatorRuntime>> getAvailableIOSRuntimes() async => <IOSSimulatorRuntime>[];
325
}
326

327
class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter {
328
  @override
329 330 331
  bool get isInstalled => true;

  @override
332
  String get versionText => 'Xcode 14';
333 334

  @override
335
  Version get version => Version(14, null, null);
336

337
  @override
338
  String get build => '14A309';
339

340
  @override
341
  Future<Map<String, String>> getBuildSettings(
342
    String projectPath, {
343
    XcodeProjectBuildContext? buildContext,
344 345 346 347 348
    Duration timeout = const Duration(minutes: 1),
  }) async {
    return <String, String>{};
  }

349 350 351 352 353
  @override
  Future<String> pluginsBuildSettingsOutput(
      Directory podXcodeProject, {
        Duration timeout = const Duration(minutes: 1),
      }) async {
354
    return '';
355 356
  }

357
  @override
358
  Future<void> cleanWorkspace(String workspacePath, String scheme, { bool verbose = false }) async { }
359

360
  @override
361
  Future<XcodeProjectInfo> getInfo(String projectPath, {String? projectFilename}) async {
362
    return XcodeProjectInfo(
363 364 365
      <String>['Runner'],
      <String>['Debug', 'Release'],
      <String>['Runner'],
366
      BufferLogger.test(),
367 368
    );
  }
369 370 371

  @override
  List<String> xcrunCommand() => <String>['xcrun'];
372 373
}

374 375 376 377 378 379 380
/// 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 { }
}
381

382
class LocalFileSystemBlockingSetCurrentDirectory extends LocalFileSystem {
383 384 385
  LocalFileSystemBlockingSetCurrentDirectory() : super.test(
    signals: LocalSignals.instance,
  );
386

387 388
  @override
  set currentDirectory(dynamic value) {
389
    throw Exception('globals.fs.currentDirectory should not be set on the local file system during '
390 391
          'tests as this can cause race conditions with concurrent tests. '
          'Consider using a MemoryFileSystem for testing if possible or refactor '
392
          'code to not require setting globals.fs.currentDirectory.');
393 394
  }
}
395 396 397 398

class FakeSignals implements Signals {
  @override
  Object addHandler(ProcessSignal signal, SignalHandler handler) {
399
    return const Object();
400 401 402 403 404 405 406 407 408 409
  }

  @override
  Future<bool> removeHandler(ProcessSignal signal, Object token) async {
    return true;
  }

  @override
  Stream<Object> get errors => const Stream<Object>.empty();
}