context.dart 12.7 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
// @dart = 2.8

7 8
import 'dart:async';

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

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

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

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

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

53 54
FakeDeviceManager get testDeviceManager => context.get<DeviceManager>() as FakeDeviceManager;
FakeDoctor get testDoctor => context.get<Doctor>() as FakeDoctor;
55

56
typedef ContextInitializer = void Function(AppContext testContext);
57

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

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

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

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

188
class FakeDeviceManager implements DeviceManager {
189 190
  List<Device> devices = <Device>[];

191 192 193 194
  String _specifiedDeviceId;

  @override
  String get specifiedDeviceId {
195
    if (_specifiedDeviceId == null || _specifiedDeviceId == 'all') {
196
      return null;
197
    }
198 199 200
    return _specifiedDeviceId;
  }

201
  @override
202 203 204
  set specifiedDeviceId(String id) {
    _specifiedDeviceId = id;
  }
205 206

  @override
207 208
  bool get hasSpecifiedDeviceId => specifiedDeviceId != null;

209 210 211 212 213
  @override
  bool get hasSpecifiedAllDevices {
    return _specifiedDeviceId != null && _specifiedDeviceId == 'all';
  }

214
  @override
215
  Future<List<Device>> getAllConnectedDevices() async => devices;
216

217 218 219
  @override
  Future<List<Device>> refreshAllConnectedDevices({ Duration timeout }) async => devices;

220
  @override
221 222
  Future<List<Device>> getDevicesById(String deviceId) async {
    return devices.where((Device device) => device.id == deviceId).toList();
223 224
  }

225
  @override
226
  Future<List<Device>> getDevices() {
227 228 229
    return hasSpecifiedDeviceId
        ? getDevicesById(specifiedDeviceId)
        : getAllConnectedDevices();
230 231 232
  }

  void addDevice(Device device) => devices.add(device);
233 234 235 236 237 238

  @override
  bool get canListAnything => true;

  @override
  Future<List<String>> getDeviceDiagnostics() async => <String>[];
239 240 241

  @override
  List<DeviceDiscovery> get deviceDiscoverers => <DeviceDiscovery>[];
242 243

  @override
244 245
  bool isDeviceSupportedForProject(Device device, FlutterProject flutterProject) {
    return device.isSupportedForProject(flutterProject);
246 247 248
  }

  @override
249
  Future<List<Device>> findTargetDevices(FlutterProject flutterProject, { Duration timeout }) async {
250
    return devices;
251
  }
252 253
}

254
class FakeAndroidLicenseValidator extends AndroidLicenseValidator {
255 256 257 258
  @override
  Future<LicensesAccepted> get licensesAccepted async => LicensesAccepted.all;
}

259
class FakeDoctor extends Doctor {
260 261
  FakeDoctor(Logger logger) : super(logger: logger);

262 263 264 265
  // True for testing.
  @override
  bool get canListAnything => true;

266
  // True for testing.
267
  @override
268
  bool get canLaunchAnything => true;
269 270 271 272 273 274 275

  @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;
276
    return superValidators.map<DoctorValidator>((DoctorValidator v) {
277
      if (v is AndroidLicenseValidator) {
278
        return FakeAndroidLicenseValidator();
279 280 281 282
      }
      return v;
    }).toList();
  }
283
}
284

285 286
class NoopIOSSimulatorUtils implements IOSSimulatorUtils {
  const NoopIOSSimulatorUtils();
287

288 289 290
  @override
  Future<List<IOSSimulator>> getAttachedDevices() async => <IOSSimulator>[];
}
291

292
class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter {
293
  @override
294 295 296
  bool get isInstalled => true;

  @override
297
  String get versionText => 'Xcode 13';
298 299

  @override
300
  Version get version => Version(13, null, null);
301

302
  @override
303
  Future<Map<String, String>> getBuildSettings(
304
    String projectPath, {
305
    XcodeProjectBuildContext buildContext,
306 307 308 309 310
    Duration timeout = const Duration(minutes: 1),
  }) async {
    return <String, String>{};
  }

311 312 313 314 315 316 317 318
  @override
  Future<String> pluginsBuildSettingsOutput(
      Directory podXcodeProject, {
        Duration timeout = const Duration(minutes: 1),
      }) async {
    return null;
  }

319
  @override
320
  Future<void> cleanWorkspace(String workspacePath, String scheme, { bool verbose = false }) async { }
321

322
  @override
323
  Future<XcodeProjectInfo> getInfo(String projectPath, {String projectFilename}) async {
324
    return XcodeProjectInfo(
325 326 327
      <String>['Runner'],
      <String>['Debug', 'Release'],
      <String>['Runner'],
328
      BufferLogger.test(),
329 330
    );
  }
331 332 333

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

336 337 338 339 340 341 342
/// 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 { }
}
343

344
class LocalFileSystemBlockingSetCurrentDirectory extends LocalFileSystem {
345 346 347
  LocalFileSystemBlockingSetCurrentDirectory() : super.test(
    signals: LocalSignals.instance,
  );
348

349 350
  @override
  set currentDirectory(dynamic value) {
351
    throw Exception('globals.fs.currentDirectory should not be set on the local file system during '
352 353
          'tests as this can cause race conditions with concurrent tests. '
          'Consider using a MemoryFileSystem for testing if possible or refactor '
354
          'code to not require setting globals.fs.currentDirectory.');
355 356
  }
}
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371

class FakeSignals implements Signals {
  @override
  Object addHandler(ProcessSignal signal, SignalHandler handler) {
    return null;
  }

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

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