daemon_test.dart 23 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Devon Carew's avatar
Devon Carew committed
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

Devon Carew's avatar
Devon Carew committed
7 8
import 'dart:async';

9
import 'package:fake_async/fake_async.dart';
10
import 'package:flutter_tools/src/android/android_device.dart';
11
import 'package:flutter_tools/src/android/android_workflow.dart';
12
import 'package:flutter_tools/src/base/common.dart';
13
import 'package:flutter_tools/src/base/logger.dart';
14
import 'package:flutter_tools/src/base/utils.dart';
15
import 'package:flutter_tools/src/build_info.dart';
16
import 'package:flutter_tools/src/commands/daemon.dart';
17
import 'package:flutter_tools/src/device.dart';
18
import 'package:flutter_tools/src/features.dart';
19
import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
20
import 'package:flutter_tools/src/globals_null_migrated.dart' as globals;
21
import 'package:flutter_tools/src/ios/ios_workflow.dart';
22
import 'package:flutter_tools/src/resident_runner.dart';
23
import 'package:test/fake.dart';
Devon Carew's avatar
Devon Carew committed
24

25 26
import '../../src/common.dart';
import '../../src/context.dart';
27
import '../../src/fake_devices.dart';
28
import '../../src/fakes.dart';
Devon Carew's avatar
Devon Carew committed
29

30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
/// Runs a callback using FakeAsync.run while continually pumping the
/// microtask queue. This avoids a deadlock when tests `await` a Future
/// which queues a microtask that will not be processed unless the queue
/// is flushed.
Future<T> _runFakeAsync<T>(Future<T> Function(FakeAsync time) f) async {
  return FakeAsync().run((FakeAsync time) async {
    bool pump = true;
    final Future<T> future = f(time).whenComplete(() => pump = false);
    while (pump) {
      time.flushMicrotasks();
    }
    return future;
  });
}

45
void main() {
46 47
  Daemon daemon;
  NotifyingLogger notifyingLogger;
48
  BufferLogger bufferLogger;
49 50

  group('daemon', () {
51
    setUp(() {
52 53
      bufferLogger = BufferLogger.test();
      notifyingLogger = NotifyingLogger(verbose: false, parent: bufferLogger);
54
    });
Devon Carew's avatar
Devon Carew committed
55 56

    tearDown(() {
57
      if (daemon != null) {
Devon Carew's avatar
Devon Carew committed
58
        return daemon.shutdown();
59
      }
60
      notifyingLogger.dispose();
Devon Carew's avatar
Devon Carew committed
61 62
    });

63
    testUsingContext('daemon.version command should succeed', () async {
64 65 66
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
Devon Carew's avatar
Devon Carew committed
67
        commands.stream,
68
        responses.add,
69
        notifyingLogger: notifyingLogger,
Devon Carew's avatar
Devon Carew committed
70
      );
Ian Hickson's avatar
Ian Hickson committed
71
      commands.add(<String, dynamic>{'id': 0, 'method': 'daemon.version'});
72
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
Devon Carew's avatar
Devon Carew committed
73 74
      expect(response['id'], 0);
      expect(response['result'], isNotEmpty);
Dan Field's avatar
Dan Field committed
75
      expect(response['result'], isA<String>());
76 77
      await responses.close();
      await commands.close();
Devon Carew's avatar
Devon Carew committed
78 79
    });

80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
    testUsingContext('daemon.getSupportedPlatforms command should succeed', () async {
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
        commands.stream,
        responses.add,
        notifyingLogger: notifyingLogger,
      );
      // Use the flutter_gallery project which has a known set of supported platforms.
      final String projectPath = globals.fs.path.join(getFlutterRoot(), 'dev', 'integration_tests', 'flutter_gallery');

      commands.add(<String, dynamic>{'id': 0, 'method': 'daemon.getSupportedPlatforms', 'params': <String, Object>{'projectRoot': projectPath}});
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);

      expect(response['id'], 0);
      expect(response['result'], isNotEmpty);
96
      expect((response['result'] as Map<String, dynamic>)['platforms'], <String>{'macos'});
97 98 99 100 101 102 103
      await responses.close();
      await commands.close();
    }, overrides: <Type, Generator>{
      // Disable Android/iOS and enable macOS to make sure result is consistent and defaults are tested off.
      FeatureFlags: () => TestFeatureFlags(isAndroidEnabled: false, isIOSEnabled: false, isMacOSEnabled: true),
    });

104
    testUsingContext('printError should send daemon.logMessage event', () async {
105 106 107
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
108 109 110 111
        commands.stream,
        responses.add,
        notifyingLogger: notifyingLogger,
      );
112
      globals.printError('daemon.logMessage test');
113
      final Map<String, dynamic> response = await responses.stream.firstWhere((Map<String, dynamic> map) {
114
        return map['event'] == 'daemon.logMessage' && (map['params'] as Map<String, dynamic>)['level'] == 'error';
115
      });
116 117
      expect(response['id'], isNull);
      expect(response['event'], 'daemon.logMessage');
118
      final Map<String, String> logMessage = castStringKeyedMap(response['params']).cast<String, String>();
119 120
      expect(logMessage['level'], 'error');
      expect(logMessage['message'], 'daemon.logMessage test');
121 122
      await responses.close();
      await commands.close();
123 124
    }, overrides: <Type, Generator>{
      Logger: () => notifyingLogger,
125 126
    });

127
    testUsingContext('printStatus should log to stdout when logToStdout is enabled', () async {
128
      final StringBuffer buffer = await capturedConsolePrint(() {
129 130 131
        final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
        final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
        daemon = Daemon(
132 133 134 135 136
          commands.stream,
          responses.add,
          notifyingLogger: notifyingLogger,
          logToStdout: true,
        );
137
        globals.printStatus('daemon.logMessage test');
138 139
        return Future<void>.value();
      });
140 141

      expect(buffer.toString().trim(), 'daemon.logMessage test');
142 143
    }, overrides: <Type, Generator>{
      Logger: () => notifyingLogger,
144 145
    });

146
    testUsingContext('daemon.shutdown command should stop daemon', () async {
147 148 149
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
Devon Carew's avatar
Devon Carew committed
150
        commands.stream,
151
        responses.add,
152
        notifyingLogger: notifyingLogger,
Devon Carew's avatar
Devon Carew committed
153
      );
154
      commands.add(<String, dynamic>{'id': 0, 'method': 'daemon.shutdown'});
155
      return daemon.onExit.then<void>((int code) async {
156
        await commands.close();
Devon Carew's avatar
Devon Carew committed
157 158 159 160
        expect(code, 0);
      });
    });

161
    testUsingContext('app.restart without an appId should report an error', () async {
162 163 164
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
165
        commands.stream,
166
        responses.add,
167
        notifyingLogger: notifyingLogger,
168 169
      );

170
      commands.add(<String, dynamic>{'id': 0, 'method': 'app.restart'});
171
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
172 173
      expect(response['id'], 0);
      expect(response['error'], contains('appId is required'));
174 175
      await responses.close();
      await commands.close();
176 177
    });

178
    testUsingContext('ext.flutter.debugPaint via service extension without an appId should report an error', () async {
179 180 181
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
182 183 184
        commands.stream,
        responses.add,
        notifyingLogger: notifyingLogger,
185 186 187 188 189
      );

      commands.add(<String, dynamic>{
        'id': 0,
        'method': 'app.callServiceExtension',
190
        'params': <String, String>{
191 192
          'methodName': 'ext.flutter.debugPaint',
        },
193
      });
194
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
195 196
      expect(response['id'], 0);
      expect(response['error'], contains('appId is required'));
197 198
      await responses.close();
      await commands.close();
199 200
    });

201
    testUsingContext('app.stop without appId should report an error', () async {
202 203 204
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
Devon Carew's avatar
Devon Carew committed
205
        commands.stream,
206
        responses.add,
207
        notifyingLogger: notifyingLogger,
Devon Carew's avatar
Devon Carew committed
208 209
      );

210
      commands.add(<String, dynamic>{'id': 0, 'method': 'app.stop'});
211
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
Devon Carew's avatar
Devon Carew committed
212
      expect(response['id'], 0);
213
      expect(response['error'], contains('appId is required'));
214 215
      await responses.close();
      await commands.close();
Devon Carew's avatar
Devon Carew committed
216
    });
217

218
    testUsingContext('device.getDevices should respond with list', () async {
219 220 221
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
222
        commands.stream,
223
        responses.add,
224
        notifyingLogger: notifyingLogger,
225
      );
226
      commands.add(<String, dynamic>{'id': 0, 'method': 'device.getDevices'});
227
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
228 229
      expect(response['id'], 0);
      expect(response['result'], isList);
230 231
      await responses.close();
      await commands.close();
232
    });
233

Chris Bracken's avatar
Chris Bracken committed
234
    testUsingContext('device.getDevices reports available devices', () async {
235 236 237 238 239 240 241
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
        commands.stream,
        responses.add,
        notifyingLogger: notifyingLogger,
      );
242
      final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
243
      daemon.deviceDomain.addDeviceDiscoverer(discoverer);
244
      discoverer.addDevice(FakeAndroidDevice());
245 246 247 248 249 250 251 252 253 254
      commands.add(<String, dynamic>{'id': 0, 'method': 'device.getDevices'});
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
      expect(response['id'], 0);
      final dynamic result = response['result'];
      expect(result, isList);
      expect(result, isNotEmpty);
      await responses.close();
      await commands.close();
    });

255
    testUsingContext('should send device.added event when device is discovered', () async {
256 257 258
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
259 260 261
        commands.stream,
        responses.add,
        notifyingLogger: notifyingLogger,
262 263
      );

264
      final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
265
      daemon.deviceDomain.addDeviceDiscoverer(discoverer);
266
      discoverer.addDevice(FakeAndroidDevice());
267

268
      return responses.stream.skipWhile(_isConnectedEvent).first.then<void>((Map<String, dynamic> response) async {
269 270 271
        expect(response['event'], 'device.added');
        expect(response['params'], isMap);

272
        final Map<String, dynamic> params = castStringKeyedMap(response['params']);
273
        expect(params['platform'], isNotEmpty); // the fake device has a platform of 'android-arm'
274

275 276
        await responses.close();
        await commands.close();
277
      });
278
    }, overrides: <Type, Generator>{
279 280 281
      AndroidWorkflow: () => FakeAndroidWorkflow(),
      IOSWorkflow: () => FakeIOSWorkflow(),
      FuchsiaWorkflow: () => FakeFuchsiaWorkflow(),
282
    });
283 284

    testUsingContext('emulator.launch without an emulatorId should report an error', () async {
285 286 287
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
288 289
        commands.stream,
        responses.add,
290
        notifyingLogger: notifyingLogger,
291 292
      );

293
      commands.add(<String, dynamic>{'id': 0, 'method': 'emulator.launch'});
294 295 296
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
      expect(response['id'], 0);
      expect(response['error'], contains('emulatorId is required'));
297 298
      await responses.close();
      await commands.close();
299 300
    });

301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
    testUsingContext('emulator.launch coldboot parameter must be boolean', () async {
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
        commands.stream,
        responses.add,
        notifyingLogger: notifyingLogger,
      );
      final Map<String, dynamic> params = <String, dynamic>{'emulatorId': 'device', 'coldBoot': 1};
      commands.add(<String, dynamic>{'id': 0, 'method': 'emulator.launch', 'params': params});
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
      expect(response['id'], 0);
      expect(response['error'], contains('coldBoot is not a bool'));
      await responses.close();
      await commands.close();
    });

318
    testUsingContext('emulator.getEmulators should respond with list', () async {
319 320 321
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
322 323
        commands.stream,
        responses.add,
324
        notifyingLogger: notifyingLogger,
325 326 327 328 329
      );
      commands.add(<String, dynamic>{'id': 0, 'method': 'emulator.getEmulators'});
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
      expect(response['id'], 0);
      expect(response['result'], isList);
330 331
      await responses.close();
      await commands.close();
332
    });
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349

    testUsingContext('daemon can send exposeUrl requests to the client', () async {
      const String originalUrl = 'http://localhost:1234/';
      const String mappedUrl = 'https://publichost:4321/';
      final StreamController<Map<String, dynamic>> input = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> output = StreamController<Map<String, dynamic>>();

      daemon = Daemon(
        input.stream,
        output.add,
        notifyingLogger: notifyingLogger,
      );

      // Respond to any requests from the daemon to expose a URL.
      unawaited(output.stream
        .firstWhere((Map<String, dynamic> request) => request['method'] == 'app.exposeUrl')
        .then((Map<String, dynamic> request) {
350
          expect((request['params'] as Map<String, dynamic>)['url'], equals(originalUrl));
351 352 353 354 355 356 357 358 359 360
          input.add(<String, dynamic>{'id': request['id'], 'result': <String, dynamic>{'url': mappedUrl}});
        })
      );

      final String exposedUrl = await daemon.daemonDomain.exposeUrl(originalUrl);
      expect(exposedUrl, equals(mappedUrl));

      await output.close();
      await input.close();
    });
361

362
    testUsingContext('devtools.serve command should return host and port on success', () async {
363 364 365 366 367 368 369 370
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
        commands.stream,
        responses.add,
        notifyingLogger: notifyingLogger,
      );

371 372
      commands.add(<String, dynamic>{'id': 0, 'method': 'devtools.serve'});
      final Map<String, dynamic> response = await responses.stream.firstWhere((Map<String, dynamic> response) => response['id'] == 0);
373 374 375 376
      final Map<String, dynamic> result = response['result'] as Map<String, dynamic>;
      expect(result, isNotEmpty);
      expect(result['host'], '127.0.0.1');
      expect(result['port'], 1234);
377 378 379
      await responses.close();
      await commands.close();
    }, overrides: <Type, Generator>{
380
      DevtoolsLauncher: () => FakeDevtoolsLauncher(DevToolsServerAddress('127.0.0.1', 1234)),
381 382 383 384 385 386 387 388 389 390
    });

    testUsingContext('devtools.serve command should return null fields if null returned', () async {
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
        commands.stream,
        responses.add,
        notifyingLogger: notifyingLogger,
      );
391 392

      commands.add(<String, dynamic>{'id': 0, 'method': 'devtools.serve'});
393
      final Map<String, dynamic> response = await responses.stream.firstWhere((Map<String, dynamic> response) => response['id'] == 0);
394 395 396 397
      final Map<String, dynamic> result = response['result'] as Map<String, dynamic>;
      expect(result, isNotEmpty);
      expect(result['host'], null);
      expect(result['port'], null);
398 399 400
      await responses.close();
      await commands.close();
    }, overrides: <Type, Generator>{
401
      DevtoolsLauncher: () => FakeDevtoolsLauncher(null),
402
    });
Devon Carew's avatar
Devon Carew committed
403
  });
404

405
  testUsingContext('notifyingLogger outputs trace messages in verbose mode', () async {
406
    final NotifyingLogger logger = NotifyingLogger(verbose: true, parent: bufferLogger);
407 408 409

    logger.printTrace('test');

410
    expect(bufferLogger.errorText, contains('test'));
411 412 413
  });

  testUsingContext('notifyingLogger ignores trace messages in non-verbose mode', () async {
414
    final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger);
415 416 417 418 419 420 421 422 423

    final Future<LogMessage> messageResult = logger.onMessage.first;
    logger.printTrace('test');
    logger.printStatus('hello');

    final LogMessage message = await messageResult;

    expect(message.level, 'status');
    expect(message.message, 'hello');
424
    expect(bufferLogger.errorText, contains('test'));
425 426 427
  });

  testUsingContext('notifyingLogger buffers messages sent before a subscription', () async {
428
    final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger);
429 430 431 432 433 434 435 436 437

    logger.printStatus('hello');

    final LogMessage message = await logger.onMessage.first;

    expect(message.level, 'status');
    expect(message.message, 'hello');
  });

438 439 440 441
  group('daemon serialization', () {
    test('OperationResult', () {
      expect(
        jsonEncodeObject(OperationResult.ok),
442
        '{"code":0,"message":""}',
443 444
      );
      expect(
445
        jsonEncodeObject(OperationResult(1, 'foo')),
446
        '{"code":1,"message":"foo"}',
447 448 449
      );
    });
  });
450 451 452 453 454 455 456 457 458 459 460 461

  group('daemon queue', () {
    DebounceOperationQueue<int, String> queue;
    const Duration debounceDuration = Duration(seconds: 1);

    setUp(() {
      queue = DebounceOperationQueue<int, String>();
    });

    testWithoutContext(
        'debounces/merges same operation type and returns same result',
        () async {
462
      await _runFakeAsync((FakeAsync time) async {
463 464 465 466 467 468 469 470 471 472 473 474 475 476
        final List<Future<int>> operations = <Future<int>>[
          queue.queueAndDebounce('OP1', debounceDuration, () async => 1),
          queue.queueAndDebounce('OP1', debounceDuration, () async => 2),
        ];

        time.elapse(debounceDuration * 5);
        final List<int> results = await Future.wait(operations);

        expect(results, orderedEquals(<int>[1, 1]));
      });
    });

    testWithoutContext('does not merge results outside of the debounce duration',
        () async {
477
      await _runFakeAsync((FakeAsync time) async {
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492
        final List<Future<int>> operations = <Future<int>>[
          queue.queueAndDebounce('OP1', debounceDuration, () async => 1),
          Future<int>.delayed(debounceDuration * 2).then((_) =>
              queue.queueAndDebounce('OP1', debounceDuration, () async => 2)),
        ];

        time.elapse(debounceDuration * 5);
        final List<int> results = await Future.wait(operations);

        expect(results, orderedEquals(<int>[1, 2]));
      });
    });

    testWithoutContext('does not merge results of different operations',
        () async {
493
      await _runFakeAsync((FakeAsync time) async {
494 495 496 497 498 499 500 501 502 503 504 505 506
        final List<Future<int>> operations = <Future<int>>[
          queue.queueAndDebounce('OP1', debounceDuration, () async => 1),
          queue.queueAndDebounce('OP2', debounceDuration, () async => 2),
        ];

        time.elapse(debounceDuration * 5);
        final List<int> results = await Future.wait(operations);

        expect(results, orderedEquals(<int>[1, 2]));
      });
    });

    testWithoutContext('does not run any operations concurrently', () async {
507
      // Crete a function that's slow, but throws if another instance of the
508 509 510 511 512 513 514 515 516 517 518 519
      // function is running.
      bool isRunning = false;
      Future<int> f(int ret) async {
        if (isRunning) {
          throw 'Functions ran concurrently!';
        }
        isRunning = true;
        await Future<void>.delayed(debounceDuration * 2);
        isRunning = false;
        return ret;
      }

520
      await _runFakeAsync((FakeAsync time) async {
521 522 523 524 525 526 527 528 529 530 531 532
        final List<Future<int>> operations = <Future<int>>[
          queue.queueAndDebounce('OP1', debounceDuration, () => f(1)),
          queue.queueAndDebounce('OP2', debounceDuration, () => f(2)),
        ];

        time.elapse(debounceDuration * 5);
        final List<int> results = await Future.wait(operations);

        expect(results, orderedEquals(<int>[1, 2]));
      });
    });
  });
Devon Carew's avatar
Devon Carew committed
533
}
534 535

bool _notEvent(Map<String, dynamic> map) => map['event'] == null;
536

537 538
bool _isConnectedEvent(Map<String, dynamic> map) => map['event'] == 'daemon.connected';

539 540
class FakeFuchsiaWorkflow extends Fake implements FuchsiaWorkflow {
  FakeFuchsiaWorkflow({ this.canListDevices = true });
541 542 543 544 545

  @override
  final bool canListDevices;
}

546 547
class FakeAndroidWorkflow extends Fake implements AndroidWorkflow {
  FakeAndroidWorkflow({ this.canListDevices = true });
548

549 550 551 552
  @override
  final bool canListDevices;
}

553 554
class FakeIOSWorkflow extends Fake implements IOSWorkflow {
  FakeIOSWorkflow({ this.canListDevices = true });
555

556 557 558
  @override
  final bool canListDevices;
}
559

560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596
class FakeAndroidDevice extends Fake implements AndroidDevice {
  @override
  final String id = 'device';

  @override
  final String name = 'device';

  @override
  Future<String> get emulatorId async => 'device';

  @override
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.android_arm;

  @override
  Future<bool> get isLocalEmulator async => false;

  @override
  final Category category = Category.mobile;

  @override
  final PlatformType platformType = PlatformType.android;

  @override
  final bool ephemeral = false;
}

class FakeDevtoolsLauncher extends Fake implements DevtoolsLauncher {
  FakeDevtoolsLauncher(this._serverAddress);

  final DevToolsServerAddress _serverAddress;

  @override
  Future<DevToolsServerAddress> serve() async => _serverAddress;

  @override
  Future<void> close() async {}
}