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

5 6
// @dart = 2.8

7 8 9 10 11 12
// TODO(gspencergoog): Remove this tag once this test's state leaks/test
// dependencies have been fixed.
// https://github.com/flutter/flutter/issues/85160
// Fails with "flutter test --test-randomize-ordering-seed=1000"
@Tags(<String>['no-shuffle'])

Devon Carew's avatar
Devon Carew committed
13 14
import 'dart:async';

15
import 'package:fake_async/fake_async.dart';
16
import 'package:flutter_tools/src/android/android_device.dart';
17
import 'package:flutter_tools/src/android/android_workflow.dart';
18
import 'package:flutter_tools/src/base/common.dart';
19
import 'package:flutter_tools/src/base/logger.dart';
20
import 'package:flutter_tools/src/base/utils.dart';
21
import 'package:flutter_tools/src/build_info.dart';
22
import 'package:flutter_tools/src/commands/daemon.dart';
23
import 'package:flutter_tools/src/daemon.dart';
24
import 'package:flutter_tools/src/device.dart';
25
import 'package:flutter_tools/src/features.dart';
26
import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
27
import 'package:flutter_tools/src/globals.dart' as globals;
28
import 'package:flutter_tools/src/ios/ios_workflow.dart';
29
import 'package:flutter_tools/src/resident_runner.dart';
30
import 'package:test/fake.dart';
Devon Carew's avatar
Devon Carew committed
31

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

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

52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
class FakeDaemonStreams extends DaemonStreams {
  final StreamController<Map<String, dynamic>> inputs = StreamController<Map<String, dynamic>>();
  final StreamController<Map<String, dynamic>> outputs = StreamController<Map<String, dynamic>>();

  @override
  Stream<Map<String, dynamic>> get inputStream {
    return inputs.stream;
  }

  @override
  void send(Map<String, dynamic> message) {
    outputs.add(message);
  }

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

74
void main() {
75 76
  Daemon daemon;
  NotifyingLogger notifyingLogger;
77
  BufferLogger bufferLogger;
78 79

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

92
    tearDown(() async {
93
      if (daemon != null) {
Devon Carew's avatar
Devon Carew committed
94
        return daemon.shutdown();
95
      }
96
      notifyingLogger.dispose();
97
      await daemonConnection.dispose();
Devon Carew's avatar
Devon Carew committed
98 99
    });

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

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

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

      expect(response['id'], 0);
      expect(response['result'], isNotEmpty);
125
      expect((response['result'] as Map<String, dynamic>)['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 Map<String, dynamic> response = await daemonStreams.outputs.stream.firstWhere((Map<String, dynamic> map) {
138
        return map['event'] == 'daemon.logMessage' && (map['params'] as Map<String, dynamic>)['level'] == 'error';
139
      });
140 141
      expect(response['id'], isNull);
      expect(response['event'], 'daemon.logMessage');
142
      final Map<String, String> logMessage = castStringKeyedMap(response['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 Map<String, dynamic> response = await daemonStreams.outputs.stream.firstWhere((Map<String, dynamic> map) {
156 157 158 159 160 161 162 163
        return map['event'] == 'daemon.logMessage' && (map['params'] as Map<String, dynamic>)['level'] == 'warning';
      });
      expect(response['id'], isNull);
      expect(response['event'], 'daemon.logMessage');
      final Map<String, String> logMessage = castStringKeyedMap(response['params']).cast<String, String>();
      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(<String, dynamic>{'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 218
      daemonStreams.inputs.add(<String, dynamic>{'id': 0, 'method': 'app.restart'});
      final Map<String, dynamic> response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
219 220 221 222
      expect(response['id'], 0);
      expect(response['error'], contains('appId is required'));
    });

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(<String, dynamic>{
230 231
        'id': 0,
        'method': 'app.callServiceExtension',
232
        'params': <String, String>{
233 234
          'methodName': 'ext.flutter.debugPaint',
        },
235
      });
236
      final Map<String, dynamic> response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
237 238 239 240
      expect(response['id'], 0);
      expect(response['error'], contains('appId is required'));
    });

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 248
      daemonStreams.inputs.add(<String, dynamic>{'id': 0, 'method': 'app.stop'});
      final Map<String, dynamic> response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
Devon Carew's avatar
Devon Carew committed
249
      expect(response['id'], 0);
250
      expect(response['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 259
      daemonStreams.inputs.add(<String, dynamic>{'id': 0, 'method': 'device.getDevices'});
      final Map<String, dynamic> response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
260 261 262
      expect(response['id'], 0);
      expect(response['result'], isList);
    });
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 273
      daemonStreams.inputs.add(<String, dynamic>{'id': 0, 'method': 'device.getDevices'});
      final Map<String, dynamic> response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
274 275 276 277 278 279
      expect(response['id'], 0);
      final dynamic result = response['result'];
      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
      return daemonStreams.outputs.stream.skipWhile(_isConnectedEvent).first.then<void>((Map<String, dynamic> response) async {
291 292 293
        expect(response['event'], 'device.added');
        expect(response['params'], isMap);

294
        final Map<String, dynamic> params = castStringKeyedMap(response['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

    testUsingContext('emulator.launch without an emulatorId should report an error', () async {
304
      daemon = Daemon(
305
        daemonConnection,
306
        notifyingLogger: notifyingLogger,
307 308
      );

309 310
      daemonStreams.inputs.add(<String, dynamic>{'id': 0, 'method': 'emulator.launch'});
      final Map<String, dynamic> response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
311 312 313 314
      expect(response['id'], 0);
      expect(response['error'], contains('emulatorId is required'));
    });

315 316
    testUsingContext('emulator.launch coldboot parameter must be boolean', () async {
      daemon = Daemon(
317
        daemonConnection,
318 319 320
        notifyingLogger: notifyingLogger,
      );
      final Map<String, dynamic> params = <String, dynamic>{'emulatorId': 'device', 'coldBoot': 1};
321 322
      daemonStreams.inputs.add(<String, dynamic>{'id': 0, 'method': 'emulator.launch', 'params': params});
      final Map<String, dynamic> response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
323 324 325 326
      expect(response['id'], 0);
      expect(response['error'], contains('coldBoot is not a bool'));
    });

327
    testUsingContext('emulator.getEmulators should respond with list', () async {
328
      daemon = Daemon(
329
        daemonConnection,
330
        notifyingLogger: notifyingLogger,
331
      );
332 333
      daemonStreams.inputs.add(<String, dynamic>{'id': 0, 'method': 'emulator.getEmulators'});
      final Map<String, dynamic> response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
334 335 336
      expect(response['id'], 0);
      expect(response['result'], isList);
    });
337 338 339 340 341 342

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

      daemon = Daemon(
343
        daemonConnection,
344 345 346 347
        notifyingLogger: notifyingLogger,
      );

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

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

360
    testUsingContext('devtools.serve command should return host and port on success', () async {
361
      daemon = Daemon(
362
        daemonConnection,
363 364 365
        notifyingLogger: notifyingLogger,
      );

366 367
      daemonStreams.inputs.add(<String, dynamic>{'id': 0, 'method': 'devtools.serve'});
      final Map<String, dynamic> response = await daemonStreams.outputs.stream.firstWhere((Map<String, dynamic> response) => response['id'] == 0);
368 369 370 371
      final Map<String, dynamic> result = response['result'] as Map<String, dynamic>;
      expect(result, isNotEmpty);
      expect(result['host'], '127.0.0.1');
      expect(result['port'], 1234);
372
    }, overrides: <Type, Generator>{
373
      DevtoolsLauncher: () => FakeDevtoolsLauncher(DevToolsServerAddress('127.0.0.1', 1234)),
374 375 376 377
    });

    testUsingContext('devtools.serve command should return null fields if null returned', () async {
      daemon = Daemon(
378
        daemonConnection,
379 380
        notifyingLogger: notifyingLogger,
      );
381

382 383
      daemonStreams.inputs.add(<String, dynamic>{'id': 0, 'method': 'devtools.serve'});
      final Map<String, dynamic> response = await daemonStreams.outputs.stream.firstWhere((Map<String, dynamic> response) => response['id'] == 0);
384 385 386 387
      final Map<String, dynamic> result = response['result'] as Map<String, dynamic>;
      expect(result, isNotEmpty);
      expect(result['host'], null);
      expect(result['port'], null);
388
    }, overrides: <Type, Generator>{
389
      DevtoolsLauncher: () => FakeDevtoolsLauncher(null),
390
    });
Devon Carew's avatar
Devon Carew committed
391
  });
392

393
  testUsingContext('notifyingLogger outputs trace messages in verbose mode', () async {
394
    final NotifyingLogger logger = NotifyingLogger(verbose: true, parent: bufferLogger);
395 396 397

    logger.printTrace('test');

398
    expect(bufferLogger.errorText, contains('test'));
399 400 401
  });

  testUsingContext('notifyingLogger ignores trace messages in non-verbose mode', () async {
402
    final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger);
403 404 405 406 407 408 409 410 411

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

    final LogMessage message = await messageResult;

    expect(message.level, 'status');
    expect(message.message, 'hello');
412
    expect(bufferLogger.errorText, contains('test'));
413 414 415
  });

  testUsingContext('notifyingLogger buffers messages sent before a subscription', () async {
416
    final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger);
417 418 419 420 421 422 423 424 425

    logger.printStatus('hello');

    final LogMessage message = await logger.onMessage.first;

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

426 427 428 429 430 431 432 433 434 435 436
  group('daemon queue', () {
    DebounceOperationQueue<int, String> queue;
    const Duration debounceDuration = Duration(seconds: 1);

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

    testWithoutContext(
        'debounces/merges same operation type and returns same result',
        () async {
437
      await _runFakeAsync((FakeAsync time) async {
438 439 440 441 442 443 444 445 446 447 448 449 450 451
        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 {
452
      await _runFakeAsync((FakeAsync time) async {
453 454 455 456 457 458 459 460 461 462 463 464 465 466 467
        final List<Future<int>> operations = <Future<int>>[
          queue.queueAndDebounce('OP1', debounceDuration, () async => 1),
          Future<int>.delayed(debounceDuration * 2).then((_) =>
              queue.queueAndDebounce('OP1', debounceDuration, () async => 2)),
        ];

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

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

    testWithoutContext('does not merge results of different operations',
        () async {
468
      await _runFakeAsync((FakeAsync time) async {
469 470 471 472 473 474 475 476 477 478 479 480 481
        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 {
482
      // Crete a function that's slow, but throws if another instance of the
483 484 485 486 487 488 489 490 491 492 493 494
      // function is running.
      bool isRunning = false;
      Future<int> f(int ret) async {
        if (isRunning) {
          throw 'Functions ran concurrently!';
        }
        isRunning = true;
        await Future<void>.delayed(debounceDuration * 2);
        isRunning = false;
        return ret;
      }

495
      await _runFakeAsync((FakeAsync time) async {
496 497 498 499 500 501 502 503 504 505 506 507
        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
508
}
509 510

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

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

514 515
class FakeFuchsiaWorkflow extends Fake implements FuchsiaWorkflow {
  FakeFuchsiaWorkflow({ this.canListDevices = true });
516 517 518 519 520

  @override
  final bool canListDevices;
}

521 522
class FakeAndroidWorkflow extends Fake implements AndroidWorkflow {
  FakeAndroidWorkflow({ this.canListDevices = true });
523

524 525 526 527
  @override
  final bool canListDevices;
}

528 529
class FakeIOSWorkflow extends Fake implements IOSWorkflow {
  FakeIOSWorkflow({ this.canListDevices = true });
530

531 532 533
  @override
  final bool canListDevices;
}
534

535 536 537
// 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
538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
class FakeAndroidDevice extends Fake implements AndroidDevice {
  @override
  final String id = 'device';

  @override
  final String name = 'device';

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

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

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

  @override
  final Category category = Category.mobile;

  @override
  final PlatformType platformType = PlatformType.android;

  @override
  final bool ephemeral = false;
}

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

  final DevToolsServerAddress _serverAddress;

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

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