daemon_test.dart 37.8 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/src/interface/file.dart';
11
import 'package:flutter_tools/src/android/android_device.dart';
12
import 'package:flutter_tools/src/android/android_workflow.dart';
13
import 'package:flutter_tools/src/application_package.dart';
14
import 'package:flutter_tools/src/base/logger.dart';
15
import 'package:flutter_tools/src/base/utils.dart';
16
import 'package:flutter_tools/src/build_info.dart';
17
import 'package:flutter_tools/src/commands/daemon.dart';
18
import 'package:flutter_tools/src/daemon.dart';
19
import 'package:flutter_tools/src/device.dart';
20
import 'package:flutter_tools/src/features.dart';
21
import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
22
import 'package:flutter_tools/src/globals.dart' as globals;
23
import 'package:flutter_tools/src/ios/ios_workflow.dart';
24
import 'package:flutter_tools/src/resident_runner.dart';
25
import 'package:flutter_tools/src/vmservice.dart';
26
import 'package:test/fake.dart';
Devon Carew's avatar
Devon Carew committed
27

28 29
import '../../src/common.dart';
import '../../src/context.dart';
30
import '../../src/fake_devices.dart';
31
import '../../src/fakes.dart';
Devon Carew's avatar
Devon Carew committed
32

33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
/// 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;
  });
}

48 49 50
class FakeDaemonStreams implements DaemonStreams {
  final StreamController<DaemonMessage> inputs = StreamController<DaemonMessage>();
  final StreamController<DaemonMessage> outputs = StreamController<DaemonMessage>();
51 52

  @override
53
  Stream<DaemonMessage> get inputStream {
54 55 56 57
    return inputs.stream;
  }

  @override
58
  void send(Map<String, Object?> message, [ List<int>? binary ]) {
59
    outputs.add(DaemonMessage(message, binary != null ? Stream<List<int>>.value(binary) : null));
60 61 62 63 64 65 66 67 68 69
  }

  @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());
  }
}

70
void main() {
71 72
  late Daemon daemon;
  late NotifyingLogger notifyingLogger;
73 74

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

88
    tearDown(() async {
89
      if (daemon != null) {
Devon Carew's avatar
Devon Carew committed
90
        return daemon.shutdown();
91
      }
92
      notifyingLogger.dispose();
93
      await daemonConnection.dispose();
Devon Carew's avatar
Devon Carew committed
94 95
    });

96
    testUsingContext('daemon.version command should succeed', () async {
97
      daemon = Daemon(
98
        daemonConnection,
99
        notifyingLogger: notifyingLogger,
Devon Carew's avatar
Devon Carew committed
100
      );
101
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'daemon.version'}));
102 103 104 105
      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
106 107
    });

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

116
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
117 118 119 120 121 122 123 124
        '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);
125
      expect((response.data['result']! as Map<String, Object?>)['platforms'], <String>{'macos'});
126 127 128 129 130
    }, 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),
    });

131
    testUsingContext('printError should send daemon.logMessage event', () async {
132
      daemon = Daemon(
133
        daemonConnection,
134 135
        notifyingLogger: notifyingLogger,
      );
136
      globals.printError('daemon.logMessage test');
137
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere((DaemonMessage message) {
138
        return message.data['event'] == 'daemon.logMessage' && (message.data['params']! as Map<String, Object?>)['level'] == 'error';
139
      });
140 141
      expect(response.data['id'], isNull);
      expect(response.data['event'], 'daemon.logMessage');
142
      final Map<String, String> logMessage = castStringKeyedMap(response.data['params'])!.cast<String, String>();
143 144 145
      expect(logMessage['level'], 'error');
      expect(logMessage['message'], 'daemon.logMessage test');
    }, overrides: <Type, Generator>{
146 147 148 149 150
      Logger: () => notifyingLogger,
    });

    testUsingContext('printWarning should send daemon.logMessage event', () async {
      daemon = Daemon(
151
        daemonConnection,
152 153 154
        notifyingLogger: notifyingLogger,
      );
      globals.printWarning('daemon.logMessage test');
155
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere((DaemonMessage message) {
156
        return message.data['event'] == 'daemon.logMessage' && (message.data['params']! as Map<String, Object?>)['level'] == 'warning';
157
      });
158 159
      expect(response.data['id'], isNull);
      expect(response.data['event'], 'daemon.logMessage');
160
      final Map<String, String> logMessage = castStringKeyedMap(response.data['params'])!.cast<String, String>();
161 162 163
      expect(logMessage['level'], 'warning');
      expect(logMessage['message'], 'daemon.logMessage test');
    }, overrides: <Type, Generator>{
164
      Logger: () => notifyingLogger,
165 166
    });

167
    testUsingContext('printStatus should log to stdout when logToStdout is enabled', () async {
168
      final StringBuffer buffer = await capturedConsolePrint(() {
169
        daemon = Daemon(
170
          daemonConnection,
171 172 173
          notifyingLogger: notifyingLogger,
          logToStdout: true,
        );
174
        globals.printStatus('daemon.logMessage test');
175 176
        return Future<void>.value();
      });
177 178

      expect(buffer.toString().trim(), 'daemon.logMessage test');
179 180
    }, overrides: <Type, Generator>{
      Logger: () => notifyingLogger,
181 182
    });

183 184 185
    testUsingContext('printBox should log to stdout when logToStdout is enabled', () async {
      final StringBuffer buffer = await capturedConsolePrint(() {
        daemon = Daemon(
186
          daemonConnection,
187 188 189 190 191 192 193 194 195 196 197 198
          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,
    });

199
    testUsingContext('daemon.shutdown command should stop daemon', () async {
200
      daemon = Daemon(
201
        daemonConnection,
202
        notifyingLogger: notifyingLogger,
Devon Carew's avatar
Devon Carew committed
203
      );
204
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'daemon.shutdown'}));
205
      return daemon.onExit.then<void>((int code) async {
206
        await daemonStreams.inputs.close();
Devon Carew's avatar
Devon Carew committed
207 208 209 210
        expect(code, 0);
      });
    });

211
    testUsingContext('app.restart without an appId should report an error', () async {
212
      daemon = Daemon(
213
        daemonConnection,
214
        notifyingLogger: notifyingLogger,
215 216
      );

217
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'app.restart'}));
218 219 220
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
      expect(response.data['error'], contains('appId is required'));
221 222
    });

223
    testUsingContext('ext.flutter.debugPaint via service extension without an appId should report an error', () async {
224
      daemon = Daemon(
225
        daemonConnection,
226
        notifyingLogger: notifyingLogger,
227 228
      );

229
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
230 231
        'id': 0,
        'method': 'app.callServiceExtension',
232
        'params': <String, String>{
233 234
          'methodName': 'ext.flutter.debugPaint',
        },
235 236 237 238
      }));
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
      expect(response.data['error'], contains('appId is required'));
239 240
    });

241
    testUsingContext('app.stop without appId should report an error', () async {
242
      daemon = Daemon(
243
        daemonConnection,
244
        notifyingLogger: notifyingLogger,
Devon Carew's avatar
Devon Carew committed
245 246
      );

247
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'app.stop'}));
248 249 250
      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
251
    });
252

253
    testUsingContext('device.getDevices should respond with list', () async {
254
      daemon = Daemon(
255
        daemonConnection,
256
        notifyingLogger: notifyingLogger,
257
      );
258
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'device.getDevices'}));
259 260 261
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
      expect(response.data['result'], isList);
262
    });
263

Chris Bracken's avatar
Chris Bracken committed
264
    testUsingContext('device.getDevices reports available devices', () async {
265
      daemon = Daemon(
266
        daemonConnection,
267 268
        notifyingLogger: notifyingLogger,
      );
269
      final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
270
      daemon.deviceDomain.addDeviceDiscoverer(discoverer);
271
      discoverer.addDevice(FakeAndroidDevice());
272
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'device.getDevices'}));
273 274
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
275
      final Object? result = response.data['result'];
276 277 278 279
      expect(result, isList);
      expect(result, isNotEmpty);
    });

280
    testUsingContext('should send device.added event when device is discovered', () async {
281
      daemon = Daemon(
282
        daemonConnection,
283
        notifyingLogger: notifyingLogger,
284 285
      );

286
      final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
287
      daemon.deviceDomain.addDeviceDiscoverer(discoverer);
288
      discoverer.addDevice(FakeAndroidDevice());
289

290 291 292
      return daemonStreams.outputs.stream.skipWhile(_isConnectedEvent).first.then<void>((DaemonMessage response) async {
        expect(response.data['event'], 'device.added');
        expect(response.data['params'], isMap);
293

294
        final Map<String, Object?> params = castStringKeyedMap(response.data['params'])!;
295
        expect(params['platform'], isNotEmpty); // the fake device has a platform of 'android-arm'
296
      });
297
    }, overrides: <Type, Generator>{
298 299 300
      AndroidWorkflow: () => FakeAndroidWorkflow(),
      IOSWorkflow: () => FakeIOSWorkflow(),
      FuchsiaWorkflow: () => FakeFuchsiaWorkflow(),
301
    });
302

303 304 305 306 307
    testUsingContext('device.discoverDevices should respond with list', () async {
      daemon = Daemon(
        daemonConnection,
        notifyingLogger: notifyingLogger,
      );
308
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'device.discoverDevices'}));
309 310 311 312 313 314 315 316 317 318 319 320 321
      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());
322
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'device.discoverDevices'}));
323 324
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
325
      final Object? result = response.data['result'];
326 327 328 329 330 331 332 333 334 335 336 337 338 339
      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);
340
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
341 342
        'id': 0,
        'method': 'device.supportsRuntimeMode',
343
        'params': <String, Object?>{
344 345 346 347 348 349
          'deviceId': 'device',
          'buildMode': 'profile',
        },
      }));
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
350
      final Object? result = response.data['result'];
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
      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;
366
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
367 368
        'id': 0,
        'method': 'device.logReader.start',
369
        'params': <String, Object?>{
370 371 372 373 374 375
          'deviceId': 'device',
        },
      }));
      final Stream<DaemonMessage> broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream();
      final DaemonMessage firstResponse = await broadcastOutput.firstWhere(_notEvent);
      expect(firstResponse.data['id'], 0);
376
      final String? logReaderId = firstResponse.data['result'] as String?;
377 378 379 380 381 382 383 384 385 386 387
      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);
388
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
389 390
        'id': 1,
        'method': 'device.logReader.stop',
391
        'params': <String, Object?>{
392 393 394 395 396 397 398 399 400
          'id': logReaderId,
        },
      }));
      final DaemonMessage stopResponse = await broadcastOutput.firstWhere(_notEvent);
      expect(stopResponse.data['id'], 1);
      expect(logReader.disposeCalled, true);
    });

    group('device.startApp and .stopApp', () {
401
      late FakeApplicationPackageFactory applicationPackageFactory;
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419
      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;
420
        daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
421 422
          'id': 0,
          'method': 'device.uploadApplicationPackage',
423
          'params': <String, Object?>{
424 425 426 427 428 429
            'targetPlatform': 'android',
            'applicationBinary': 'test_file',
          },
        }));
        final DaemonMessage applicationPackageIdResponse = await broadcastOutput.firstWhere(_notEvent);
        expect(applicationPackageIdResponse.data['id'], 0);
430
        expect(applicationPackageFactory.applicationBinaryRequested!.basename, 'test_file');
431
        expect(applicationPackageFactory.platformRequested, TargetPlatform.android);
432
        final String? applicationPackageId = applicationPackageIdResponse.data['result'] as String?;
433 434 435 436

        // Try starting the app.
        final Uri observatoryUri = Uri.parse('http://127.0.0.1:12345/observatory');
        device.launchResult = LaunchResult.succeeded(observatoryUri: observatoryUri);
437
        daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
438 439
          'id': 1,
          'method': 'device.startApp',
440
          'params': <String, Object?>{
441 442 443 444 445 446 447 448
            '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);
449
        final Map<String, Object?> startAppResult = startAppResponse.data['result']! as Map<String, Object?>;
450 451 452 453
        expect(startAppResult['started'], true);
        expect(startAppResult['observatoryUri'], observatoryUri.toString());

        // Try stopping the app.
454
        daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
455 456
          'id': 2,
          'method': 'device.stopApp',
457
          'params': <String, Object?>{
458 459 460 461 462 463 464
            'deviceId': 'device',
            'applicationPackageId': applicationPackageId,
          },
        }));
        final DaemonMessage stopAppResponse = await broadcastOutput.firstWhere(_notEvent);
        expect(stopAppResponse.data['id'], 2);
        expect(device.stopAppPackage, applicationPackage);
465
        final bool? stopAppResult = stopAppResponse.data['result'] as bool?;
466 467 468 469 470 471
        expect(stopAppResult, true);
      }, overrides: <Type, Generator>{
        ApplicationPackageFactory: () => applicationPackageFactory,
      });
    });

472
    testUsingContext('emulator.launch without an emulatorId should report an error', () async {
473
      daemon = Daemon(
474
        daemonConnection,
475
        notifyingLogger: notifyingLogger,
476 477
      );

478
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'emulator.launch'}));
479 480 481
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
      expect(response.data['error'], contains('emulatorId is required'));
482 483
    });

484 485
    testUsingContext('emulator.launch coldboot parameter must be boolean', () async {
      daemon = Daemon(
486
        daemonConnection,
487 488
        notifyingLogger: notifyingLogger,
      );
489 490
      final Map<String, Object?> params = <String, Object?>{'emulatorId': 'device', 'coldBoot': 1};
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'emulator.launch', 'params': params}));
491 492 493
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
      expect(response.data['error'], contains('coldBoot is not a bool'));
494 495
    });

496
    testUsingContext('emulator.getEmulators should respond with list', () async {
497
      daemon = Daemon(
498
        daemonConnection,
499
        notifyingLogger: notifyingLogger,
500
      );
501
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'emulator.getEmulators'}));
502 503 504
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
      expect(response.data['id'], 0);
      expect(response.data['result'], isList);
505
    });
506 507 508 509 510 511

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

      daemon = Daemon(
512
        daemonConnection,
513 514 515 516
        notifyingLogger: notifyingLogger,
      );

      // Respond to any requests from the daemon to expose a URL.
517
      unawaited(daemonStreams.outputs.stream
518 519
        .firstWhere((DaemonMessage request) => request.data['method'] == 'app.exposeUrl')
        .then((DaemonMessage request) {
520 521
          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}}));
522 523 524 525 526 527
        })
      );

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

529
    testUsingContext('devtools.serve command should return host and port on success', () async {
530
      daemon = Daemon(
531
        daemonConnection,
532 533 534
        notifyingLogger: notifyingLogger,
      );

535
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'devtools.serve'}));
536
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere((DaemonMessage response) => response.data['id'] == 0);
537
      final Map<String, Object?> result = response.data['result']! as Map<String, Object?>;
538 539 540
      expect(result, isNotEmpty);
      expect(result['host'], '127.0.0.1');
      expect(result['port'], 1234);
541
    }, overrides: <Type, Generator>{
542
      DevtoolsLauncher: () => FakeDevtoolsLauncher(DevToolsServerAddress('127.0.0.1', 1234)),
543 544 545 546
    });

    testUsingContext('devtools.serve command should return null fields if null returned', () async {
      daemon = Daemon(
547
        daemonConnection,
548 549
        notifyingLogger: notifyingLogger,
      );
550

551
      daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'devtools.serve'}));
552
      final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere((DaemonMessage response) => response.data['id'] == 0);
553
      final Map<String, Object?> result = response.data['result']! as Map<String, Object?>;
554 555 556
      expect(result, isNotEmpty);
      expect(result['host'], null);
      expect(result['port'], null);
557
    }, overrides: <Type, Generator>{
558
      DevtoolsLauncher: () => FakeDevtoolsLauncher(null),
559
    });
560 561 562 563 564 565

    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;
566 567
        int? connectPort;
        ioOverrides.connectCallback = (Object? host, int port) async {
568 569 570 571 572 573 574 575 576 577 578 579
          connectCalled = true;
          connectPort = port;
          if (host == io.InternetAddress.loopbackIPv4) {
            return socket;
          }
          throw const io.SocketException('fail');
        };

        daemon = Daemon(
          daemonConnection,
          notifyingLogger: notifyingLogger,
        );
580
        daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'proxy.connect', 'params': <String, Object?>{'port': 123}}));
581 582 583 584 585 586 587 588

        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);

589
        final Object? id = firstResponse.data['result'];
590 591 592 593 594 595 596

        // 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);
597
        final List<List<int>> data = await dataEvent.binary!.toList();
598 599 600
        expect(data[0], <int>[10, 11, 12]);

        // Can proxy data to the socket.
601
        daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'proxy.write', 'params': <String, Object?>{'id': id}}, Stream<List<int>>.value(<int>[21, 22, 23])));
602 603 604 605 606
        await pumpEventQueue();
        expect(socket.addedData[0], <int>[21, 22, 23]);

        // Closes the connection when disconnect request received.
        expect(socket.closeCalled, false);
607
        daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'proxy.disconnect', 'params': <String, Object?>{'id': id}}));
608 609 610 611
        await pumpEventQueue();
        expect(socket.closeCalled, true);

        // Sends disconnected event when socket.done completer finishes.
612
        socket.doneCompleter.complete(true);
613 614 615 616 617 618 619 620 621 622 623 624
        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;
625 626
        int? connectPort;
        ioOverrides.connectCallback = (Object? host, int port) async {
627 628 629 630 631 632 633 634 635 636 637 638 639
          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,
        );
640
        daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'proxy.connect', 'params': <String, Object?>{'port': 123}}));
641 642 643 644 645 646 647 648 649 650 651 652 653

        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 {
654
        ioOverrides.connectCallback = (Object? host, int port) => throw const io.SocketException('fail');
655 656 657 658 659

        daemon = Daemon(
          daemonConnection,
          notifyingLogger: notifyingLogger,
        );
660
        daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'proxy.connect', 'params': <String, Object?>{'port': 123}}));
661 662 663 664 665 666 667 668

        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
669
  });
670

671
  group('notifyingLogger', () {
672
    late BufferLogger bufferLogger;
673 674 675
    setUp(() {
      bufferLogger = BufferLogger.test();
    });
676

677 678 679
    tearDown(() {
      bufferLogger.clear();
    });
680

681 682 683 684 685
    testUsingContext('outputs trace messages in verbose mode', () async {
      final NotifyingLogger logger = NotifyingLogger(verbose: true, parent: bufferLogger);
      logger.printTrace('test');
      expect(bufferLogger.errorText, contains('test'));
    });
686

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

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

694
      final LogMessage message = await messageResult;
695

696 697 698 699
      expect(message.level, 'status');
      expect(message.message, 'hello');
      expect(bufferLogger.errorText, isEmpty);
    });
700

701 702
    testUsingContext('buffers messages sent before a subscription', () async {
      final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger);
703

704
      logger.printStatus('hello');
705

706
      final LogMessage message = await logger.onMessage.first;
707

708 709 710
      expect(message.level, 'status');
      expect(message.message, 'hello');
    });
711 712 713 714 715

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

718
  group('daemon queue', () {
719
    late DebounceOperationQueue<int, String> queue;
720 721 722 723 724 725 726 727 728
    const Duration debounceDuration = Duration(seconds: 1);

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

    testWithoutContext(
        'debounces/merges same operation type and returns same result',
        () async {
729
      await _runFakeAsync((FakeAsync time) async {
730 731 732 733 734 735 736 737 738 739 740 741 742 743
        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 {
744
      await _runFakeAsync((FakeAsync time) async {
745 746
        final List<Future<int>> operations = <Future<int>>[
          queue.queueAndDebounce('OP1', debounceDuration, () async => 1),
747
          Future<void>.delayed(debounceDuration * 2).then((_) =>
748 749 750 751 752 753 754 755 756 757 758 759
              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 {
760
      await _runFakeAsync((FakeAsync time) async {
761 762 763 764 765 766 767 768 769 770 771 772 773
        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 {
774
      // Crete a function that's slow, but throws if another instance of the
775 776 777 778
      // function is running.
      bool isRunning = false;
      Future<int> f(int ret) async {
        if (isRunning) {
779
          throw Exception('Functions ran concurrently!');
780 781 782 783 784 785 786
        }
        isRunning = true;
        await Future<void>.delayed(debounceDuration * 2);
        isRunning = false;
        return ret;
      }

787
      await _runFakeAsync((FakeAsync time) async {
788 789 790 791 792 793 794 795 796 797 798 799
        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
800
}
801

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

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

806 807
class FakeFuchsiaWorkflow extends Fake implements FuchsiaWorkflow {
  FakeFuchsiaWorkflow({ this.canListDevices = true });
808 809 810 811 812

  @override
  final bool canListDevices;
}

813 814
class FakeAndroidWorkflow extends Fake implements AndroidWorkflow {
  FakeAndroidWorkflow({ this.canListDevices = true });
815

816 817 818 819
  @override
  final bool canListDevices;
}

820 821
class FakeIOSWorkflow extends Fake implements IOSWorkflow {
  FakeIOSWorkflow({ this.canListDevices = true });
822

823 824 825
  @override
  final bool canListDevices;
}
826

827 828 829
// Unfortunately Device, despite not being immutable, has an `operator ==`.
// Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types
830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853
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;
854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878

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

879
  BuildMode? supportsRuntimeModeCalledBuildMode;
880 881 882 883 884 885
  @override
  Future<bool> supportsRuntimeMode(BuildMode buildMode) async {
    supportsRuntimeModeCalledBuildMode = buildMode;
    return true;
  }

886
  late DeviceLogReader logReader;
887 888
  @override
  FutureOr<DeviceLogReader> getLogReader({
889
    ApplicationPackage? app,
890 891 892
    bool includePastLogs = false,
  }) => logReader;

893 894
  ApplicationPackage? startAppPackage;
  late LaunchResult launchResult;
895

896 897
  @override
  Future<LaunchResult> startApp(
898
    ApplicationPackage? package, {
899 900 901 902
    String? mainPath,
    String? route,
    DebuggingOptions? debuggingOptions,
    Map<String, Object?> platformArgs = const <String, Object>{},
903 904
    bool prebuiltApplication = false,
    bool ipv6 = false,
905
    String? userIdentifier,
906 907 908 909 910
  }) async {
    startAppPackage = package;
    return launchResult;
  }

911
  ApplicationPackage? stopAppPackage;
912 913
  @override
  Future<bool> stopApp(
914
    ApplicationPackage? app, {
915
    String? userIdentifier,
916 917 918 919 920 921 922 923 924 925 926
  }) async {
    stopAppPackage = app;
    return true;
  }
}

class FakeDeviceLogReader implements DeviceLogReader {
  final StreamController<String> logLinesController = StreamController<String>();
  bool disposeCalled = false;

  @override
927
  int? appPid;
928 929

  @override
930
  FlutterVmService? connectedVMService;
931 932 933 934 935 936 937 938 939 940 941 942

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

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

  @override
  String get name => 'device';

943 944 945 946 947
}

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

948
  final DevToolsServerAddress? _serverAddress;
949 950

  @override
951
  Future<DevToolsServerAddress?> serve() async => _serverAddress;
952 953 954 955

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

class FakeApplicationPackageFactory implements ApplicationPackageFactory {
958 959 960
  TargetPlatform? platformRequested;
  File? applicationBinaryRequested;
  ApplicationPackage? applicationPackage;
961 962

  @override
963
  Future<ApplicationPackage?> getPackageForPlatform(TargetPlatform platform, {BuildInfo? buildInfo, File? applicationBinary}) async {
964 965 966 967 968 969 970
    platformRequested = platform;
    applicationBinaryRequested = applicationBinary;
    return applicationPackage;
  }
}

class FakeApplicationPackage extends Fake implements ApplicationPackage {}
971 972

class TestIOOverrides extends io.IOOverrides {
973
  late Future<io.Socket> Function(Object? host, int port) connectCallback;
974 975

  @override
976 977
  Future<io.Socket> socketConnect(Object? host, int port,
      {Object? sourceAddress, int sourcePort = 0, Duration? timeout}) {
978 979 980 981 982 983 984 985 986 987 988 989
    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(
990 991 992 993
    void Function(Uint8List event)? onData, {
    Function? onError,
    void Function()? onDone,
    bool? cancelOnError,
994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013
  }) {
    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() {}
}