daemon_test.dart 48.9 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 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6 7
import 'dart:io' as io;
import 'dart:typed_data';
Devon Carew's avatar
Devon Carew committed
8

9
import 'package:fake_async/fake_async.dart';
10
import 'package:file/memory.dart';
11
import 'package:file/src/interface/file.dart';
12
import 'package:flutter_tools/src/android/android_device.dart';
13
import 'package:flutter_tools/src/android/android_workflow.dart';
14
import 'package:flutter_tools/src/application_package.dart';
15
import 'package:flutter_tools/src/artifacts.dart';
16
import 'package:flutter_tools/src/base/dds.dart';
17
import 'package:flutter_tools/src/base/logger.dart';
18
import 'package:flutter_tools/src/base/utils.dart';
19
import 'package:flutter_tools/src/build_info.dart';
20
import 'package:flutter_tools/src/commands/daemon.dart';
21
import 'package:flutter_tools/src/daemon.dart';
22
import 'package:flutter_tools/src/device.dart';
23
import 'package:flutter_tools/src/features.dart';
24
import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
25
import 'package:flutter_tools/src/globals.dart' as globals;
26
import 'package:flutter_tools/src/ios/ios_workflow.dart';
27
import 'package:flutter_tools/src/preview_device.dart';
28
import 'package:flutter_tools/src/resident_runner.dart';
29
import 'package:flutter_tools/src/vmservice.dart';
30
import 'package:flutter_tools/src/windows/windows_workflow.dart';
31
import 'package:test/fake.dart';
Devon Carew's avatar
Devon Carew committed
32

33 34
import '../../src/common.dart';
import '../../src/context.dart';
35
import '../../src/fake_devices.dart';
36
import '../../src/fakes.dart';
Devon Carew's avatar
Devon Carew committed
37

38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
/// 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;
  });
}

53 54 55
class FakeDaemonStreams implements DaemonStreams {
  final StreamController<DaemonMessage> inputs = StreamController<DaemonMessage>();
  final StreamController<DaemonMessage> outputs = StreamController<DaemonMessage>();
56 57

  @override
58
  Stream<DaemonMessage> get inputStream {
59 60 61 62
    return inputs.stream;
  }

  @override
63
  void send(Map<String, Object?> message, [ List<int>? binary ]) {
64
    outputs.add(DaemonMessage(message, binary != null ? Stream<List<int>>.value(binary) : null));
65 66 67 68 69 70 71 72 73 74
  }

  @override
  Future<void> dispose() async {
    await inputs.close();
    // In some tests, outputs have no listeners. We don't wait for outputs to close.
    unawaited(outputs.close());
  }
}

75
void main() {
76 77
  late Daemon daemon;
  late NotifyingLogger notifyingLogger;
78 79

  group('daemon', () {
80 81
    late FakeDaemonStreams daemonStreams;
    late DaemonConnection daemonConnection;
82
    setUp(() {
83
      BufferLogger bufferLogger;
84 85
      bufferLogger = BufferLogger.test();
      notifyingLogger = NotifyingLogger(verbose: false, parent: bufferLogger);
86 87 88 89 90
      daemonStreams = FakeDaemonStreams();
      daemonConnection = DaemonConnection(
        daemonStreams: daemonStreams,
        logger: bufferLogger,
      );
91
    });
Devon Carew's avatar
Devon Carew committed
92

93
    tearDown(() async {
94
      await daemon.shutdown();
95
      notifyingLogger.dispose();
96
      await daemonConnection.dispose();
Devon Carew's avatar
Devon Carew committed
97 98
    });

99
    testUsingContext('daemon.version command should succeed', () async {
100
      daemon = Daemon(
101
        daemonConnection,
102
        notifyingLogger: notifyingLogger,
Devon Carew's avatar
Devon Carew committed
103
      );
104
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'daemon.version'}));
105 106 107 108
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
      expect(response.data['result'], isNotEmpty);
      expect(response.data['result'], isA<String>());
Devon Carew's avatar
Devon Carew committed
109 110
    });

111 112
    testUsingContext('daemon.getSupportedPlatforms command should succeed', () async {
      daemon = Daemon(
113
        daemonConnection,
114 115 116 117 118
        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');

119
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
120 121 122 123 124 125 126 127
        'id': 0,
        'method': 'daemon.getSupportedPlatforms',
        'params': <String, Object>{'projectRoot': projectPath},
      }));
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);

      expect(response.data['id'], 0);
      expect(response.data['result'], isNotEmpty);
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
      expect(
        response.data['result']! as Map<String, Object?>,
        const <String, Object>{
          'platforms': <String>['macos', 'windows', 'windowsPreview'],
          'platformTypes': <String, Map<String, Object>>{
            'web': <String, Object>{
              'isSupported': false,
              'reasons': <Map<String, String>>[
                <String, String>{
                  'reasonText': 'the Web feature is not enabled',
                  'fixText': 'Run "flutter config --enable-web"',
                  'fixCode': 'config',
                },
              ],
            },
            'android': <String, Object>{
              'isSupported': false,
              'reasons': <Map<String, String>>[
                <String, String>{
                  'reasonText': 'the Android feature is not enabled',
                  'fixText': 'Run "flutter config --enable-android"',
                  'fixCode': 'config',
                },
              ],
            },
            'ios': <String, Object>{
              'isSupported': false,
              'reasons': <Map<String, String>>[
                <String, String>{
                  'reasonText': 'the iOS feature is not enabled',
                  'fixText': 'Run "flutter config --enable-ios"',
                  'fixCode': 'config',
                },
              ],
            },
            'linux': <String, Object>{
              'isSupported': false,
              'reasons': <Map<String, String>>[
                <String, String>{
                  'reasonText': 'the Linux feature is not enabled',
                  'fixText': 'Run "flutter config --enable-linux-desktop"',
                  'fixCode': 'config',
                },
              ],
            },
            'macos': <String, bool>{'isSupported': true},
            'windows': <String, bool>{'isSupported': true},
            'fuchsia': <String, Object>{
              'isSupported': false,
              'reasons': <Map<String, String>>[
                <String, String>{
                  'reasonText': 'the Fuchsia feature is not enabled',
                  'fixText': 'Run "flutter config --enable-fuchsia"',
                  'fixCode': 'config',
                },
                <String, String>{
                  'reasonText': 'the Fuchsia platform is not enabled for this project',
                  'fixText': 'Run "flutter create --platforms=fuchsia ." in your application directory',
                  'fixCode': 'create',
                },
              ],
            },
            'custom': <String, Object>{
              'isSupported': false,
              'reasons': <Map<String, String>>[
                <String, String>{
                  'reasonText': 'the custom devices feature is not enabled',
                  'fixText': 'Run "flutter config --enable-custom-devices"',
                  'fixCode': 'config',
                },
              ],
            },
            'windowsPreview': <String, bool>{'isSupported': true},
          },
        },
      );
204 205
    }, overrides: <Type, Generator>{
      // Disable Android/iOS and enable macOS to make sure result is consistent and defaults are tested off.
206 207 208 209 210 211 212
      FeatureFlags: () => TestFeatureFlags(
        isAndroidEnabled: false,
        isIOSEnabled: false,
        isMacOSEnabled: true,
        isPreviewDeviceEnabled: true,
        isWindowsEnabled: true,
      ),
213 214
    });

215
    testUsingContext('printError should send daemon.logMessage event', () async {
216
      daemon = Daemon(
217
        daemonConnection,
218 219
        notifyingLogger: notifyingLogger,
      );
220
      globals.printError('daemon.logMessage test');
221
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere((DaemonMessage message) {
222
        return message.data['event'] == 'daemon.logMessage' && (message.data['params']! as Map<String, Object?>)['level'] == 'error';
223
      });
224 225
      expect(response.data['id'], isNull);
      expect(response.data['event'], 'daemon.logMessage');
226
      final Map<String, String> logMessage = castStringKeyedMap(response.data['params'])!.cast<String, String>();
227 228 229
      expect(logMessage['level'], 'error');
      expect(logMessage['message'], 'daemon.logMessage test');
    }, overrides: <Type, Generator>{
230 231 232 233 234
      Logger: () => notifyingLogger,
    });

    testUsingContext('printWarning should send daemon.logMessage event', () async {
      daemon = Daemon(
235
        daemonConnection,
236 237 238
        notifyingLogger: notifyingLogger,
      );
      globals.printWarning('daemon.logMessage test');
239
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere((DaemonMessage message) {
240
        return message.data['event'] == 'daemon.logMessage' && (message.data['params']! as Map<String, Object?>)['level'] == 'warning';
241
      });
242 243
      expect(response.data['id'], isNull);
      expect(response.data['event'], 'daemon.logMessage');
244
      final Map<String, String> logMessage = castStringKeyedMap(response.data['params'])!.cast<String, String>();
245 246 247
      expect(logMessage['level'], 'warning');
      expect(logMessage['message'], 'daemon.logMessage test');
    }, overrides: <Type, Generator>{
248
      Logger: () => notifyingLogger,
249 250
    });

251
    testUsingContext('printStatus should log to stdout when logToStdout is enabled', () async {
252
      final StringBuffer buffer = await capturedConsolePrint(() {
253
        daemon = Daemon(
254
          daemonConnection,
255 256 257
          notifyingLogger: notifyingLogger,
          logToStdout: true,
        );
258
        globals.printStatus('daemon.logMessage test');
259 260
        return Future<void>.value();
      });
261 262

      expect(buffer.toString().trim(), 'daemon.logMessage test');
263 264
    }, overrides: <Type, Generator>{
      Logger: () => notifyingLogger,
265 266
    });

267 268 269
    testUsingContext('printBox should log to stdout when logToStdout is enabled', () async {
      final StringBuffer buffer = await capturedConsolePrint(() {
        daemon = Daemon(
270
          daemonConnection,
271 272 273 274 275 276 277 278 279 280 281 282
          notifyingLogger: notifyingLogger,
          logToStdout: true,
        );
        globals.printBox('This is the box message', title: 'Sample title');
        return Future<void>.value();
      });

      expect(buffer.toString().trim(), contains('Sample title: This is the box message'));
    }, overrides: <Type, Generator>{
      Logger: () => notifyingLogger,
    });

283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
    testUsingContext('printTrace should send daemon.logMessage event when notifyVerbose is enabled', () async {
      daemon = Daemon(
        daemonConnection,
        notifyingLogger: notifyingLogger,
      );
      notifyingLogger.notifyVerbose = false;
      globals.printTrace('daemon.logMessage test 1');
      notifyingLogger.notifyVerbose = true;
      globals.printTrace('daemon.logMessage test 2');
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere((DaemonMessage message) {
        return message.data['event'] == 'daemon.logMessage' && (message.data['params']! as Map<String, Object?>)['level'] == 'trace';
      });
      expect(response.data['id'], isNull);
      expect(response.data['event'], 'daemon.logMessage');
      final Map<String, String> logMessage = castStringKeyedMap(response.data['params'])!.cast<String, String>();
      expect(logMessage['level'], 'trace');
      expect(logMessage['message'], 'daemon.logMessage test 2');
    }, overrides: <Type, Generator>{
      Logger: () => notifyingLogger,
    });

    testUsingContext('daemon.setNotifyVerbose command should update the notify verbose status to true', () async {
      daemon = Daemon(
        daemonConnection,
        notifyingLogger: notifyingLogger,
      );
      expect(notifyingLogger.notifyVerbose, false);

      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
        'id': 0,
        'method': 'daemon.setNotifyVerbose',
        'params': <String, Object?>{
          'verbose': true,
        },
      }));
      await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(notifyingLogger.notifyVerbose, true);
    });

    testUsingContext('daemon.setNotifyVerbose command should update the notify verbose status to false', () async {
      daemon = Daemon(
        daemonConnection,
        notifyingLogger: notifyingLogger,
      );
      notifyingLogger.notifyVerbose = false;

      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
        'id': 0,
        'method': 'daemon.setNotifyVerbose',
        'params': <String, Object?>{
          'verbose': false,
        },
      }));
      await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(notifyingLogger.notifyVerbose, false);
    });

340
    testUsingContext('daemon.shutdown command should stop daemon', () async {
341
      daemon = Daemon(
342
        daemonConnection,
343
        notifyingLogger: notifyingLogger,
Devon Carew's avatar
Devon Carew committed
344
      );
345
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'daemon.shutdown'}));
346
      return daemon.onExit.then<void>((int code) async {
347
        await daemonStreams.inputs.close();
Devon Carew's avatar
Devon Carew committed
348 349 350 351
        expect(code, 0);
      });
    });

352
    testUsingContext('app.restart without an appId should report an error', () async {
353
      daemon = Daemon(
354
        daemonConnection,
355
        notifyingLogger: notifyingLogger,
356 357
      );

358
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'app.restart'}));
359 360 361
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
      expect(response.data['error'], contains('appId is required'));
362 363
    });

364
    testUsingContext('ext.flutter.debugPaint via service extension without an appId should report an error', () async {
365
      daemon = Daemon(
366
        daemonConnection,
367
        notifyingLogger: notifyingLogger,
368 369
      );

370
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
371 372
        'id': 0,
        'method': 'app.callServiceExtension',
373
        'params': <String, String>{
374 375
          'methodName': 'ext.flutter.debugPaint',
        },
376 377 378 379
      }));
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
      expect(response.data['error'], contains('appId is required'));
380 381
    });

382
    testUsingContext('app.stop without appId should report an error', () async {
383
      daemon = Daemon(
384
        daemonConnection,
385
        notifyingLogger: notifyingLogger,
Devon Carew's avatar
Devon Carew committed
386 387
      );

388
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'app.stop'}));
389 390 391
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
      expect(response.data['error'], contains('appId is required'));
Devon Carew's avatar
Devon Carew committed
392
    });
393

394
    testUsingContext('device.getDevices should respond with list', () async {
395
      daemon = Daemon(
396
        daemonConnection,
397
        notifyingLogger: notifyingLogger,
398
      );
399
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'device.getDevices'}));
400 401 402
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
      expect(response.data['result'], isList);
403
    });
404

Chris Bracken's avatar
Chris Bracken committed
405
    testUsingContext('device.getDevices reports available devices', () async {
406
      daemon = Daemon(
407
        daemonConnection,
408 409
        notifyingLogger: notifyingLogger,
      );
410
      final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
411
      daemon.deviceDomain.addDeviceDiscoverer(discoverer);
412
      discoverer.addDevice(FakeAndroidDevice());
413
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'device.getDevices'}));
414 415
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
416
      final Object? result = response.data['result'];
417 418 419 420
      expect(result, isList);
      expect(result, isNotEmpty);
    });

421
    testUsingContext('should send device.added event when device is discovered', () async {
422
      daemon = Daemon(
423
        daemonConnection,
424
        notifyingLogger: notifyingLogger,
425 426
      );

427
      final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
428
      daemon.deviceDomain.addDeviceDiscoverer(discoverer);
429
      discoverer.addDevice(FakeAndroidDevice());
430 431 432 433 434 435 436 437 438 439 440 441
      final MemoryFileSystem fs = MemoryFileSystem.test();
      discoverer.addDevice(PreviewDevice(
        processManager: FakeProcessManager.empty(),
        logger: BufferLogger.test(),
        fileSystem: fs,
        previewBinary: fs.file(r'preview_device.exe'),
        artifacts: Artifacts.test(fileSystem: fs),
        builderFactory: () => throw UnimplementedError('TODO implement builder factory'),
      ));

      final List<Map<String, Object?>> names = <Map<String, Object?>>[];
      await daemonStreams.outputs.stream.skipWhile(_isConnectedEvent).take(2).forEach((DaemonMessage response) async {
442 443
        expect(response.data['event'], 'device.added');
        expect(response.data['params'], isMap);
444

445
        final Map<String, Object?> params = castStringKeyedMap(response.data['params'])!;
446
        names.add(params);
447
      });
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493
      await daemonStreams.outputs.close();
      expect(
        names,
        containsAll(const <Map<String, Object?>>[
          <String, Object?>{
            'id': 'device',
            'name': 'android device',
            'platform': 'android-arm',
            'emulator': false,
            'category': 'mobile',
            'platformType': 'android',
            'ephemeral': false,
            'emulatorId': 'device',
            'sdk': 'Android 12',
            'capabilities': <String, Object?>{
              'hotReload': true,
              'hotRestart': true,
              'screenshot': true,
              'fastStart': true,
              'flutterExit': true,
              'hardwareRendering': true,
              'startPaused': true,
            },
          },
          <String, Object?>{
            'id': 'preview',
            'name': 'Preview',
            'platform': 'windows-x64',
            'emulator': false,
            'category': 'desktop',
            'platformType': 'windowsPreview',
            'ephemeral': false,
            'emulatorId': null,
            'sdk': 'preview',
            'capabilities': <String, Object?>{
              'hotReload': true,
              'hotRestart': true,
              'screenshot': false,
              'fastStart': false,
              'flutterExit': true,
              'hardwareRendering': true,
              'startPaused': true,
            },
          },
        ]),
      );
494
    }, overrides: <Type, Generator>{
495 496 497
      AndroidWorkflow: () => FakeAndroidWorkflow(),
      IOSWorkflow: () => FakeIOSWorkflow(),
      FuchsiaWorkflow: () => FakeFuchsiaWorkflow(),
498
      WindowsWorkflow: () => FakeWindowsWorkflow(),
499
    });
500

501 502 503 504 505
    testUsingContext('device.discoverDevices should respond with list', () async {
      daemon = Daemon(
        daemonConnection,
        notifyingLogger: notifyingLogger,
      );
506
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'device.discoverDevices'}));
507 508 509 510 511 512 513 514 515 516 517 518 519
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
      expect(response.data['result'], isList);
    });

    testUsingContext('device.discoverDevices reports available devices', () async {
      daemon = Daemon(
        daemonConnection,
        notifyingLogger: notifyingLogger,
      );
      final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
      daemon.deviceDomain.addDeviceDiscoverer(discoverer);
      discoverer.addDevice(FakeAndroidDevice());
520
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'device.discoverDevices'}));
521 522
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
523
      final Object? result = response.data['result'];
524 525 526 527 528 529 530 531 532 533 534 535 536 537
      expect(result, isList);
      expect(result, isNotEmpty);
      expect(discoverer.discoverDevicesCalled, true);
    });

    testUsingContext('device.supportsRuntimeMode returns correct value', () async {
      daemon = Daemon(
        daemonConnection,
        notifyingLogger: notifyingLogger,
      );
      final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
      daemon.deviceDomain.addDeviceDiscoverer(discoverer);
      final FakeAndroidDevice device = FakeAndroidDevice();
      discoverer.addDevice(device);
538
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
539 540
        'id': 0,
        'method': 'device.supportsRuntimeMode',
541
        'params': <String, Object?>{
542 543 544 545 546 547
          'deviceId': 'device',
          'buildMode': 'profile',
        },
      }));
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
548
      final Object? result = response.data['result'];
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563
      expect(result, true);
      expect(device.supportsRuntimeModeCalledBuildMode, BuildMode.profile);
    });

    testUsingContext('device.logReader.start and .stop starts and stops log reader', () async {
      daemon = Daemon(
        daemonConnection,
        notifyingLogger: notifyingLogger,
      );
      final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
      daemon.deviceDomain.addDeviceDiscoverer(discoverer);
      final FakeAndroidDevice device = FakeAndroidDevice();
      discoverer.addDevice(device);
      final FakeDeviceLogReader logReader = FakeDeviceLogReader();
      device.logReader = logReader;
564
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
565 566
        'id': 0,
        'method': 'device.logReader.start',
567
        'params': <String, Object?>{
568 569 570 571 572 573
          'deviceId': 'device',
        },
      }));
      final Stream<DaemonMessage> broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream();
      final DaemonMessage firstResponse = await broadcastOutput.firstWhere(_notEvent);
      expect(firstResponse.data['id'], 0);
574
      final String? logReaderId = firstResponse.data['result'] as String?;
575 576 577 578 579 580 581 582 583 584 585
      expect(logReaderId, isNotNull);

      // Try sending logs.
      logReader.logLinesController.add('Sample log line');
      final DaemonMessage logEvent = await broadcastOutput.firstWhere(
        (DaemonMessage message) => message.data['event'] != null && message.data['event'] != 'device.added',
      );
      expect(logEvent.data['params'], 'Sample log line');

      // Now try to stop the log reader.
      expect(logReader.disposeCalled, false);
586
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
587 588
        'id': 1,
        'method': 'device.logReader.stop',
589
        'params': <String, Object?>{
590 591 592 593 594 595 596 597 598
          'id': logReaderId,
        },
      }));
      final DaemonMessage stopResponse = await broadcastOutput.firstWhere(_notEvent);
      expect(stopResponse.data['id'], 1);
      expect(logReader.disposeCalled, true);
    });

    group('device.startApp and .stopApp', () {
599
      late FakeApplicationPackageFactory applicationPackageFactory;
600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617
      setUp(() {
        applicationPackageFactory = FakeApplicationPackageFactory();
      });

      testUsingContext('device.startApp and .stopApp starts and stops an app', () async {
        daemon = Daemon(
          daemonConnection,
          notifyingLogger: notifyingLogger,
        );
        final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
        daemon.deviceDomain.addDeviceDiscoverer(discoverer);
        final FakeAndroidDevice device = FakeAndroidDevice();
        discoverer.addDevice(device);
        final Stream<DaemonMessage> broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream();

        // First upload the application package.
        final FakeApplicationPackage applicationPackage = FakeApplicationPackage();
        applicationPackageFactory.applicationPackage = applicationPackage;
618
        daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
619 620
          'id': 0,
          'method': 'device.uploadApplicationPackage',
621
          'params': <String, Object?>{
622 623 624 625 626 627
            'targetPlatform': 'android',
            'applicationBinary': 'test_file',
          },
        }));
        final DaemonMessage applicationPackageIdResponse = await broadcastOutput.firstWhere(_notEvent);
        expect(applicationPackageIdResponse.data['id'], 0);
628
        expect(applicationPackageFactory.applicationBinaryRequested!.basename, 'test_file');
629
        expect(applicationPackageFactory.platformRequested, TargetPlatform.android);
630
        final String? applicationPackageId = applicationPackageIdResponse.data['result'] as String?;
631 632

        // Try starting the app.
633 634
        final Uri vmServiceUri = Uri.parse('http://127.0.0.1:12345/vmService');
        device.launchResult = LaunchResult.succeeded(vmServiceUri: vmServiceUri);
635
        daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
636 637
          'id': 1,
          'method': 'device.startApp',
638
          'params': <String, Object?>{
639 640 641 642 643 644 645 646
            'deviceId': 'device',
            'applicationPackageId': applicationPackageId,
            'debuggingOptions': DebuggingOptions.enabled(BuildInfo.debug).toJson(),
          },
        }));
        final DaemonMessage startAppResponse = await broadcastOutput.firstWhere(_notEvent);
        expect(startAppResponse.data['id'], 1);
        expect(device.startAppPackage, applicationPackage);
647
        final Map<String, Object?> startAppResult = startAppResponse.data['result']! as Map<String, Object?>;
648
        expect(startAppResult['started'], true);
649
        expect(startAppResult['vmServiceUri'], vmServiceUri.toString());
650 651

        // Try stopping the app.
652
        daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
653 654
          'id': 2,
          'method': 'device.stopApp',
655
          'params': <String, Object?>{
656 657 658 659 660 661 662
            'deviceId': 'device',
            'applicationPackageId': applicationPackageId,
          },
        }));
        final DaemonMessage stopAppResponse = await broadcastOutput.firstWhere(_notEvent);
        expect(stopAppResponse.data['id'], 2);
        expect(device.stopAppPackage, applicationPackage);
663
        final bool? stopAppResult = stopAppResponse.data['result'] as bool?;
664 665 666 667 668 669
        expect(stopAppResult, true);
      }, overrides: <Type, Generator>{
        ApplicationPackageFactory: () => applicationPackageFactory,
      });
    });

670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727
    testUsingContext('device.startDartDevelopmentService and .shutdownDartDevelopmentService starts and stops DDS', () async {
      daemon = Daemon(
        daemonConnection,
        notifyingLogger: notifyingLogger,
      );
      final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
      daemon.deviceDomain.addDeviceDiscoverer(discoverer);
      final FakeAndroidDevice device = FakeAndroidDevice();
      discoverer.addDevice(device);

      final Completer<void> ddsDoneCompleter = Completer<void>();
      device.dds.done = ddsDoneCompleter.future;
      final Uri fakeDdsUri = Uri.parse('http://fake_dds_uri');
      device.dds.uri = fakeDdsUri;

      // Try starting DDS.
      expect(device.dds.startCalled, false);
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
        'id': 0,
        'method': 'device.startDartDevelopmentService',
        'params': <String, Object?>{
          'deviceId': 'device',
          'disableServiceAuthCodes': false,
          'vmServiceUri': 'http://fake_uri/auth_code',
        },
      }));
      final Stream<DaemonMessage> broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream();
      final DaemonMessage startResponse = await broadcastOutput.firstWhere(_notEvent);
      expect(startResponse.data['id'], 0);
      expect(startResponse.data['error'], isNull);
      final String? ddsUri = startResponse.data['result'] as String?;
      expect(ddsUri, fakeDdsUri.toString());
      expect(device.dds.startCalled, true);
      expect(device.dds.startDisableServiceAuthCodes, false);
      expect(device.dds.startVMServiceUri, Uri.parse('http://fake_uri/auth_code'));

      // dds.done event should be sent to the client.
      ddsDoneCompleter.complete();
      final DaemonMessage startEvent = await broadcastOutput.firstWhere(
        (DaemonMessage message) => message.data['event'] != null && message.data['event'] == 'device.dds.done.device',
      );
      expect(startEvent, isNotNull);

      // Try stopping DDS.
      expect(device.dds.shutdownCalled, false);
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
        'id': 1,
        'method': 'device.shutdownDartDevelopmentService',
        'params': <String, Object?>{
          'deviceId': 'device',
        },
      }));
      final DaemonMessage stopResponse = await broadcastOutput.firstWhere(_notEvent);
      expect(stopResponse.data['id'], 1);
      expect(stopResponse.data['error'], isNull);
      expect(device.dds.shutdownCalled, true);
    });

728
    testUsingContext('emulator.launch without an emulatorId should report an error', () async {
729
      daemon = Daemon(
730
        daemonConnection,
731
        notifyingLogger: notifyingLogger,
732 733
      );

734
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'emulator.launch'}));
735 736 737
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
      expect(response.data['error'], contains('emulatorId is required'));
738 739
    });

740 741
    testUsingContext('emulator.launch coldboot parameter must be boolean', () async {
      daemon = Daemon(
742
        daemonConnection,
743 744
        notifyingLogger: notifyingLogger,
      );
745 746
      final Map<String, Object?> params = <String, Object?>{'emulatorId': 'device', 'coldBoot': 1};
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'emulator.launch', 'params': params}));
747 748 749
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
      expect(response.data['error'], contains('coldBoot is not a bool'));
750 751
    });

752
    testUsingContext('emulator.getEmulators should respond with list', () async {
753
      daemon = Daemon(
754
        daemonConnection,
755
        notifyingLogger: notifyingLogger,
756
      );
757
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'emulator.getEmulators'}));
758 759 760
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
      expect(response.data['result'], isList);
761
    });
762 763 764 765 766 767

    testUsingContext('daemon can send exposeUrl requests to the client', () async {
      const String originalUrl = 'http://localhost:1234/';
      const String mappedUrl = 'https://publichost:4321/';

      daemon = Daemon(
768
        daemonConnection,
769 770 771 772
        notifyingLogger: notifyingLogger,
      );

      // Respond to any requests from the daemon to expose a URL.
773
      unawaited(daemonStreams.outputs.stream
774 775
        .firstWhere((DaemonMessage request) => request.data['method'] == 'app.exposeUrl')
        .then((DaemonMessage request) {
776 777
          expect((request.data['params']! as Map<String, Object?>)['url'], equals(originalUrl));
          daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': request.data['id'], 'result': <String, Object?>{'url': mappedUrl}}));
778 779 780 781 782 783
        })
      );

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

785
    testUsingContext('devtools.serve command should return host and port on success', () async {
786
      daemon = Daemon(
787
        daemonConnection,
788 789 790
        notifyingLogger: notifyingLogger,
      );

791
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'devtools.serve'}));
792
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere((DaemonMessage response) => response.data['id'] == 0);
793
      final Map<String, Object?> result = response.data['result']! as Map<String, Object?>;
794 795 796
      expect(result, isNotEmpty);
      expect(result['host'], '127.0.0.1');
      expect(result['port'], 1234);
797
    }, overrides: <Type, Generator>{
798
      DevtoolsLauncher: () => FakeDevtoolsLauncher(DevToolsServerAddress('127.0.0.1', 1234)),
799 800 801 802
    });

    testUsingContext('devtools.serve command should return null fields if null returned', () async {
      daemon = Daemon(
803
        daemonConnection,
804 805
        notifyingLogger: notifyingLogger,
      );
806

807
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'devtools.serve'}));
808
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere((DaemonMessage response) => response.data['id'] == 0);
809
      final Map<String, Object?> result = response.data['result']! as Map<String, Object?>;
810 811 812
      expect(result, isNotEmpty);
      expect(result['host'], null);
      expect(result['port'], null);
813
    }, overrides: <Type, Generator>{
814
      DevtoolsLauncher: () => FakeDevtoolsLauncher(null),
815
    });
816 817 818 819 820 821

    testUsingContext('proxy.connect tries to connect to an ipv4 address and proxies the connection correctly', () async {
      final TestIOOverrides ioOverrides = TestIOOverrides();
      await io.IOOverrides.runWithIOOverrides(() async {
        final FakeSocket socket = FakeSocket();
        bool connectCalled = false;
822 823
        int? connectPort;
        ioOverrides.connectCallback = (Object? host, int port) async {
824 825 826 827 828 829 830 831 832 833 834 835
          connectCalled = true;
          connectPort = port;
          if (host == io.InternetAddress.loopbackIPv4) {
            return socket;
          }
          throw const io.SocketException('fail');
        };

        daemon = Daemon(
          daemonConnection,
          notifyingLogger: notifyingLogger,
        );
836
        daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'proxy.connect', 'params': <String, Object?>{'port': 123}}));
837 838 839 840 841 842 843 844

        final Stream<DaemonMessage> broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream();
        final DaemonMessage firstResponse = await broadcastOutput.firstWhere(_notEvent);
        expect(firstResponse.data['id'], 0);
        expect(firstResponse.data['result'], isNotNull);
        expect(connectCalled, true);
        expect(connectPort, 123);

845
        final Object? id = firstResponse.data['result'];
846 847 848 849 850 851 852

        // Can send received data as event.
        socket.controller.add(Uint8List.fromList(<int>[10, 11, 12]));
        final DaemonMessage dataEvent = await broadcastOutput.firstWhere(
          (DaemonMessage message) => message.data['event'] != null && message.data['event'] == 'proxy.data.$id',
        );
        expect(dataEvent.binary, isNotNull);
853
        final List<List<int>> data = await dataEvent.binary!.toList();
854 855 856
        expect(data[0], <int>[10, 11, 12]);

        // Can proxy data to the socket.
857
        daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'proxy.write', 'params': <String, Object?>{'id': id}}, Stream<List<int>>.value(<int>[21, 22, 23])));
858 859 860 861 862
        await pumpEventQueue();
        expect(socket.addedData[0], <int>[21, 22, 23]);

        // Closes the connection when disconnect request received.
        expect(socket.closeCalled, false);
863
        daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'proxy.disconnect', 'params': <String, Object?>{'id': id}}));
864 865 866 867
        await pumpEventQueue();
        expect(socket.closeCalled, true);

        // Sends disconnected event when socket.done completer finishes.
868
        socket.doneCompleter.complete(true);
869 870 871 872 873 874 875 876 877 878 879 880
        final DaemonMessage disconnectEvent = await broadcastOutput.firstWhere(
          (DaemonMessage message) => message.data['event'] != null && message.data['event'] == 'proxy.disconnected.$id',
        );
        expect(disconnectEvent.data, isNotNull);
      }, ioOverrides);
    });

    testUsingContext('proxy.connect connects to ipv6 if ipv4 failed', () async {
      final TestIOOverrides ioOverrides = TestIOOverrides();
      await io.IOOverrides.runWithIOOverrides(() async {
        final FakeSocket socket = FakeSocket();
        bool connectIpv4Called = false;
881 882
        int? connectPort;
        ioOverrides.connectCallback = (Object? host, int port) async {
883 884 885 886 887 888 889 890 891 892 893 894 895
          connectPort = port;
          if (host == io.InternetAddress.loopbackIPv4) {
            connectIpv4Called = true;
          } else if (host == io.InternetAddress.loopbackIPv6) {
            return socket;
          }
          throw const io.SocketException('fail');
        };

        daemon = Daemon(
          daemonConnection,
          notifyingLogger: notifyingLogger,
        );
896
        daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'proxy.connect', 'params': <String, Object?>{'port': 123}}));
897 898 899 900 901 902 903 904 905 906 907 908 909

        final Stream<DaemonMessage> broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream();
        final DaemonMessage firstResponse = await broadcastOutput.firstWhere(_notEvent);
        expect(firstResponse.data['id'], 0);
        expect(firstResponse.data['result'], isNotNull);
        expect(connectIpv4Called, true);
        expect(connectPort, 123);
      }, ioOverrides);
    });

    testUsingContext('proxy.connect fails if both ipv6 and ipv4 failed', () async {
      final TestIOOverrides ioOverrides = TestIOOverrides();
      await io.IOOverrides.runWithIOOverrides(() async {
910
        ioOverrides.connectCallback = (Object? host, int port) => throw const io.SocketException('fail');
911 912 913 914 915

        daemon = Daemon(
          daemonConnection,
          notifyingLogger: notifyingLogger,
        );
916
        daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'proxy.connect', 'params': <String, Object?>{'port': 123}}));
917 918 919 920 921 922 923 924

        final Stream<DaemonMessage> broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream();
        final DaemonMessage firstResponse = await broadcastOutput.firstWhere(_notEvent);
        expect(firstResponse.data['id'], 0);
        expect(firstResponse.data['result'], isNull);
        expect(firstResponse.data['error'], isNotNull);
      }, ioOverrides);
    });
Devon Carew's avatar
Devon Carew committed
925
  });
926

927
  group('notifyingLogger', () {
928
    late BufferLogger bufferLogger;
929 930 931
    setUp(() {
      bufferLogger = BufferLogger.test();
    });
932

933 934 935
    tearDown(() {
      bufferLogger.clear();
    });
936

937 938 939 940 941
    testUsingContext('outputs trace messages in verbose mode', () async {
      final NotifyingLogger logger = NotifyingLogger(verbose: true, parent: bufferLogger);
      logger.printTrace('test');
      expect(bufferLogger.errorText, contains('test'));
    });
942

943 944
    testUsingContext('ignores trace messages in non-verbose mode', () async {
      final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger);
945

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

950
      final LogMessage message = await messageResult;
951

952 953 954 955
      expect(message.level, 'status');
      expect(message.message, 'hello');
      expect(bufferLogger.errorText, isEmpty);
    });
956

957 958 959 960 961 962 963 964 965 966 967 968 969
    testUsingContext('sends trace messages in notify verbose mode', () async {
      final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger, notifyVerbose: true);

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

      final LogMessage message = await messageResult;

      expect(message.level, 'trace');
      expect(message.message, 'hello');
      expect(bufferLogger.errorText, isEmpty);
    });

970 971
    testUsingContext('buffers messages sent before a subscription', () async {
      final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger);
972

973
      logger.printStatus('hello');
974

975
      final LogMessage message = await logger.onMessage.first;
976

977 978 979
      expect(message.level, 'status');
      expect(message.message, 'hello');
    });
980 981 982 983 984

    testWithoutContext('responds to .supportsColor', () async {
      final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger);
      expect(logger.supportsColor, isFalse);
    });
985 986
  });

987
  group('daemon queue', () {
988
    late DebounceOperationQueue<int, String> queue;
989 990 991 992 993 994 995 996 997
    const Duration debounceDuration = Duration(seconds: 1);

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

    testWithoutContext(
        'debounces/merges same operation type and returns same result',
        () async {
998
      await _runFakeAsync((FakeAsync time) async {
999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012
        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 {
1013
      await _runFakeAsync((FakeAsync time) async {
1014 1015
        final List<Future<int>> operations = <Future<int>>[
          queue.queueAndDebounce('OP1', debounceDuration, () async => 1),
1016
          Future<void>.delayed(debounceDuration * 2).then((_) =>
1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028
              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 {
1029
      await _runFakeAsync((FakeAsync time) async {
1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042
        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 {
1043
      // Crete a function that's slow, but throws if another instance of the
1044 1045 1046 1047
      // function is running.
      bool isRunning = false;
      Future<int> f(int ret) async {
        if (isRunning) {
1048
          throw Exception('Functions ran concurrently!');
1049 1050 1051 1052 1053 1054 1055
        }
        isRunning = true;
        await Future<void>.delayed(debounceDuration * 2);
        isRunning = false;
        return ret;
      }

1056
      await _runFakeAsync((FakeAsync time) async {
1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068
        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
1069
}
1070

1071
bool _notEvent(DaemonMessage message) => message.data['event'] == null;
1072

1073
bool _isConnectedEvent(DaemonMessage message) => message.data['event'] == 'daemon.connected';
1074

1075 1076 1077 1078 1079 1080 1081
class FakeWindowsWorkflow extends Fake implements WindowsWorkflow {
  FakeWindowsWorkflow({ this.canListDevices = true });

  @override
  final bool canListDevices;
}

1082 1083
class FakeFuchsiaWorkflow extends Fake implements FuchsiaWorkflow {
  FakeFuchsiaWorkflow({ this.canListDevices = true });
1084 1085 1086 1087 1088

  @override
  final bool canListDevices;
}

1089 1090
class FakeAndroidWorkflow extends Fake implements AndroidWorkflow {
  FakeAndroidWorkflow({ this.canListDevices = true });
1091

1092 1093 1094 1095
  @override
  final bool canListDevices;
}

1096 1097
class FakeIOSWorkflow extends Fake implements IOSWorkflow {
  FakeIOSWorkflow({ this.canListDevices = true });
1098

1099 1100 1101
  @override
  final bool canListDevices;
}
1102

1103 1104 1105 1106 1107
class FakeAndroidDevice extends Fake implements AndroidDevice {
  @override
  final String id = 'device';

  @override
1108
  final String name = 'android device';
1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126

  @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;
1127

1128 1129 1130
  @override
  final bool isConnected = true;

1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154
  @override
  Future<String> get sdkNameAndVersion async => 'Android 12';

  @override
  bool get supportsHotReload => true;

  @override
  bool get supportsHotRestart => true;

  @override
  bool get supportsScreenshot => true;

  @override
  bool get supportsFastStart => true;

  @override
  bool get supportsFlutterExit => true;

  @override
  Future<bool> get supportsHardwareRendering async => true;

  @override
  bool get supportsStartPaused => true;

1155 1156 1157
  @override
  final FakeDartDevelopmentService dds = FakeDartDevelopmentService();

1158
  BuildMode? supportsRuntimeModeCalledBuildMode;
1159 1160 1161 1162 1163 1164
  @override
  Future<bool> supportsRuntimeMode(BuildMode buildMode) async {
    supportsRuntimeModeCalledBuildMode = buildMode;
    return true;
  }

1165
  late DeviceLogReader logReader;
1166 1167
  @override
  FutureOr<DeviceLogReader> getLogReader({
1168
    ApplicationPackage? app,
1169 1170 1171
    bool includePastLogs = false,
  }) => logReader;

1172 1173
  ApplicationPackage? startAppPackage;
  late LaunchResult launchResult;
1174

1175 1176
  @override
  Future<LaunchResult> startApp(
1177
    ApplicationPackage? package, {
1178 1179 1180 1181
    String? mainPath,
    String? route,
    DebuggingOptions? debuggingOptions,
    Map<String, Object?> platformArgs = const <String, Object>{},
1182 1183
    bool prebuiltApplication = false,
    bool ipv6 = false,
1184
    String? userIdentifier,
1185 1186 1187 1188 1189
  }) async {
    startAppPackage = package;
    return launchResult;
  }

1190
  ApplicationPackage? stopAppPackage;
1191 1192
  @override
  Future<bool> stopApp(
1193
    ApplicationPackage? app, {
1194
    String? userIdentifier,
1195 1196 1197 1198 1199 1200
  }) async {
    stopAppPackage = app;
    return true;
  }
}

1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233
class FakeDartDevelopmentService extends Fake implements DartDevelopmentService {
  bool startCalled = false;
  late Uri startVMServiceUri;
  bool? startDisableServiceAuthCodes;

  bool shutdownCalled = false;

  @override
  late Future<void> done;

  @override
  Uri? uri;

  @override
  Future<void> startDartDevelopmentService(
    Uri vmServiceUri, {
    required Logger logger,
    int? hostPort,
    bool? ipv6,
    bool? disableServiceAuthCodes,
    bool cacheStartupProfile = false,
  }) async {
    startCalled = true;
    startVMServiceUri = vmServiceUri;
    startDisableServiceAuthCodes = disableServiceAuthCodes;
  }

  @override
  Future<void> shutdown() async {
    shutdownCalled = true;
  }
}

1234 1235 1236 1237 1238
class FakeDeviceLogReader implements DeviceLogReader {
  final StreamController<String> logLinesController = StreamController<String>();
  bool disposeCalled = false;

  @override
1239
  int? appPid;
1240 1241

  @override
1242
  FlutterVmService? connectedVMService;
1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254

  @override
  void dispose() {
    disposeCalled = true;
  }

  @override
  Stream<String> get logLines => logLinesController.stream;

  @override
  String get name => 'device';

1255 1256 1257 1258 1259
}

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

1260
  final DevToolsServerAddress? _serverAddress;
1261 1262

  @override
1263
  Future<DevToolsServerAddress?> serve() async => _serverAddress;
1264 1265 1266 1267

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

class FakeApplicationPackageFactory implements ApplicationPackageFactory {
1270 1271 1272
  TargetPlatform? platformRequested;
  File? applicationBinaryRequested;
  ApplicationPackage? applicationPackage;
1273 1274

  @override
1275
  Future<ApplicationPackage?> getPackageForPlatform(TargetPlatform platform, {BuildInfo? buildInfo, File? applicationBinary}) async {
1276 1277 1278 1279 1280 1281 1282
    platformRequested = platform;
    applicationBinaryRequested = applicationBinary;
    return applicationPackage;
  }
}

class FakeApplicationPackage extends Fake implements ApplicationPackage {}
1283 1284

class TestIOOverrides extends io.IOOverrides {
1285
  late Future<io.Socket> Function(Object? host, int port) connectCallback;
1286 1287

  @override
1288 1289
  Future<io.Socket> socketConnect(Object? host, int port,
      {Object? sourceAddress, int sourcePort = 0, Duration? timeout}) {
1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301
    return connectCallback(host, port);
  }
}

class FakeSocket extends Fake implements io.Socket {
  bool closeCalled = false;
  final StreamController<Uint8List> controller = StreamController<Uint8List>();
  final List<List<int>> addedData = <List<int>>[];
  final Completer<bool> doneCompleter = Completer<bool>();

  @override
  StreamSubscription<Uint8List> listen(
1302 1303 1304 1305
    void Function(Uint8List event)? onData, {
    Function? onError,
    void Function()? onDone,
    bool? cancelOnError,
1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325
  }) {
    return controller.stream.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
  }

  @override
  void add(List<int> data) {
    addedData.add(data);
  }

  @override
  Future<void> close() async {
    closeCalled = true;
  }

  @override
  Future<bool> get done => doneCompleter.future;

  @override
  void destroy() {}
}