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

import 'dart:async';

7
import 'package:flutter_tools/src/android/android_workflow.dart';
8
import 'package:flutter_tools/src/base/common.dart';
9
import 'package:flutter_tools/src/base/logger.dart';
10
import 'package:flutter_tools/src/base/utils.dart';
11
import 'package:flutter_tools/src/commands/daemon.dart';
12
import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
13
import 'package:flutter_tools/src/globals.dart' as globals;
14
import 'package:flutter_tools/src/ios/ios_workflow.dart';
15
import 'package:flutter_tools/src/resident_runner.dart';
Devon Carew's avatar
Devon Carew committed
16

17 18 19
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/mocks.dart';
Devon Carew's avatar
Devon Carew committed
20

21
void main() {
22 23
  Daemon daemon;
  NotifyingLogger notifyingLogger;
24
  BufferLogger bufferLogger;
25 26

  group('daemon', () {
27
    setUp(() {
28 29
      bufferLogger = BufferLogger.test();
      notifyingLogger = NotifyingLogger(verbose: false, parent: bufferLogger);
30
    });
Devon Carew's avatar
Devon Carew committed
31 32

    tearDown(() {
33
      if (daemon != null) {
Devon Carew's avatar
Devon Carew committed
34
        return daemon.shutdown();
35
      }
36
      notifyingLogger.dispose();
Devon Carew's avatar
Devon Carew committed
37 38
    });

39
    testUsingContext('daemon.version command should succeed', () async {
40 41 42
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
Devon Carew's avatar
Devon Carew committed
43
        commands.stream,
44
        responses.add,
45
        notifyingLogger: notifyingLogger,
Devon Carew's avatar
Devon Carew committed
46
      );
Ian Hickson's avatar
Ian Hickson committed
47
      commands.add(<String, dynamic>{'id': 0, 'method': 'daemon.version'});
48
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
Devon Carew's avatar
Devon Carew committed
49 50
      expect(response['id'], 0);
      expect(response['result'], isNotEmpty);
Dan Field's avatar
Dan Field committed
51
      expect(response['result'], isA<String>());
52 53
      await responses.close();
      await commands.close();
Devon Carew's avatar
Devon Carew committed
54 55
    });

56
    testUsingContext('printError should send daemon.logMessage event', () async {
57 58 59
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
60 61 62 63
        commands.stream,
        responses.add,
        notifyingLogger: notifyingLogger,
      );
64
      globals.printError('daemon.logMessage test');
65 66
      final Map<String, dynamic> response = await responses.stream.firstWhere((Map<String, dynamic> map) {
        return map['event'] == 'daemon.logMessage' && map['params']['level'] == 'error';
67
      });
68 69
      expect(response['id'], isNull);
      expect(response['event'], 'daemon.logMessage');
70
      final Map<String, String> logMessage = castStringKeyedMap(response['params']).cast<String, String>();
71 72
      expect(logMessage['level'], 'error');
      expect(logMessage['message'], 'daemon.logMessage test');
73 74
      await responses.close();
      await commands.close();
75 76
    }, overrides: <Type, Generator>{
      Logger: () => notifyingLogger,
77 78
    });

79
    testUsingContext('printStatus should log to stdout when logToStdout is enabled', () async {
80
      final StringBuffer buffer = StringBuffer();
81

82
      await runZoned<Future<void>>(() async {
83 84 85
        final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
        final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
        daemon = Daemon(
86 87 88 89 90
          commands.stream,
          responses.add,
          notifyingLogger: notifyingLogger,
          logToStdout: true,
        );
91
        globals.printStatus('daemon.logMessage test');
92
        // Service the event loop.
93
        await Future<void>.value();
94
      }, zoneSpecification: ZoneSpecification(print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
95 96 97 98
        buffer.writeln(line);
      }));

      expect(buffer.toString().trim(), 'daemon.logMessage test');
99 100
    }, overrides: <Type, Generator>{
      Logger: () => notifyingLogger,
101 102
    });

103
    testUsingContext('daemon.shutdown command should stop daemon', () async {
104 105 106
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
Devon Carew's avatar
Devon Carew committed
107
        commands.stream,
108
        responses.add,
109
        notifyingLogger: notifyingLogger,
Devon Carew's avatar
Devon Carew committed
110
      );
111
      commands.add(<String, dynamic>{'id': 0, 'method': 'daemon.shutdown'});
112
      return daemon.onExit.then<void>((int code) async {
113
        await commands.close();
Devon Carew's avatar
Devon Carew committed
114 115 116 117
        expect(code, 0);
      });
    });

118
    testUsingContext('app.restart without an appId should report an error', () async {
119 120 121
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
122
        commands.stream,
123
        responses.add,
124
        notifyingLogger: notifyingLogger,
125 126
      );

127
      commands.add(<String, dynamic>{'id': 0, 'method': 'app.restart'});
128
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
129 130
      expect(response['id'], 0);
      expect(response['error'], contains('appId is required'));
131 132
      await responses.close();
      await commands.close();
133 134
    });

135
    testUsingContext('ext.flutter.debugPaint via service extension without an appId should report an error', () async {
136 137 138
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
139 140 141
        commands.stream,
        responses.add,
        notifyingLogger: notifyingLogger,
142 143 144 145 146
      );

      commands.add(<String, dynamic>{
        'id': 0,
        'method': 'app.callServiceExtension',
147
        'params': <String, String>{
148 149
          'methodName': 'ext.flutter.debugPaint',
        },
150
      });
151
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
152 153
      expect(response['id'], 0);
      expect(response['error'], contains('appId is required'));
154 155
      await responses.close();
      await commands.close();
156 157
    });

158
    testUsingContext('app.stop without appId should report an error', () async {
159 160 161
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
Devon Carew's avatar
Devon Carew committed
162
        commands.stream,
163
        responses.add,
164
        notifyingLogger: notifyingLogger,
Devon Carew's avatar
Devon Carew committed
165 166
      );

167
      commands.add(<String, dynamic>{'id': 0, 'method': 'app.stop'});
168
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
Devon Carew's avatar
Devon Carew committed
169
      expect(response['id'], 0);
170
      expect(response['error'], contains('appId is required'));
171 172
      await responses.close();
      await commands.close();
Devon Carew's avatar
Devon Carew committed
173
    });
174

175
    testUsingContext('device.getDevices should respond with list', () async {
176 177 178
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
179
        commands.stream,
180
        responses.add,
181
        notifyingLogger: notifyingLogger,
182
      );
183
      commands.add(<String, dynamic>{'id': 0, 'method': 'device.getDevices'});
184
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
185 186
      expect(response['id'], 0);
      expect(response['result'], isList);
187 188
      await responses.close();
      await commands.close();
189
    });
190

Chris Bracken's avatar
Chris Bracken committed
191
    testUsingContext('device.getDevices reports available devices', () async {
192 193 194 195 196 197 198
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
        commands.stream,
        responses.add,
        notifyingLogger: notifyingLogger,
      );
199
      final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
200 201 202 203 204 205 206 207 208 209 210 211
      daemon.deviceDomain.addDeviceDiscoverer(discoverer);
      discoverer.addDevice(MockAndroidDevice());
      commands.add(<String, dynamic>{'id': 0, 'method': 'device.getDevices'});
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
      expect(response['id'], 0);
      final dynamic result = response['result'];
      expect(result, isList);
      expect(result, isNotEmpty);
      await responses.close();
      await commands.close();
    });

212
    testUsingContext('should send device.added event when device is discovered', () async {
213 214 215
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
216 217 218
        commands.stream,
        responses.add,
        notifyingLogger: notifyingLogger,
219 220
      );

221
      final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
222
      daemon.deviceDomain.addDeviceDiscoverer(discoverer);
223
      discoverer.addDevice(MockAndroidDevice());
224

225
      return await responses.stream.skipWhile(_isConnectedEvent).first.then<void>((Map<String, dynamic> response) async {
226 227 228
        expect(response['event'], 'device.added');
        expect(response['params'], isMap);

229
        final Map<String, dynamic> params = castStringKeyedMap(response['params']);
230 231
        expect(params['platform'], isNotEmpty); // the mock device has a platform of 'android-arm'

232 233
        await responses.close();
        await commands.close();
234
      });
235
    }, overrides: <Type, Generator>{
236 237
      AndroidWorkflow: () => MockAndroidWorkflow(),
      IOSWorkflow: () => MockIOSWorkflow(),
238
      FuchsiaWorkflow: () => MockFuchsiaWorkflow(),
239
    });
240 241

    testUsingContext('emulator.launch without an emulatorId should report an error', () async {
242 243 244
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
245 246
        commands.stream,
        responses.add,
247
        notifyingLogger: notifyingLogger,
248 249
      );

250
      commands.add(<String, dynamic>{'id': 0, 'method': 'emulator.launch'});
251 252 253
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
      expect(response['id'], 0);
      expect(response['error'], contains('emulatorId is required'));
254 255
      await responses.close();
      await commands.close();
256 257 258
    });

    testUsingContext('emulator.getEmulators should respond with list', () async {
259 260 261
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
262 263
        commands.stream,
        responses.add,
264
        notifyingLogger: notifyingLogger,
265 266 267 268 269
      );
      commands.add(<String, dynamic>{'id': 0, 'method': 'emulator.getEmulators'});
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
      expect(response['id'], 0);
      expect(response['result'], isList);
270 271
      await responses.close();
      await commands.close();
272
    });
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300

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

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

      // Respond to any requests from the daemon to expose a URL.
      unawaited(output.stream
        .firstWhere((Map<String, dynamic> request) => request['method'] == 'app.exposeUrl')
        .then((Map<String, dynamic> request) {
          expect(request['params']['url'], equals(originalUrl));
          input.add(<String, dynamic>{'id': request['id'], 'result': <String, dynamic>{'url': mappedUrl}});
        })
      );

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

      await output.close();
      await input.close();
    });
Devon Carew's avatar
Devon Carew committed
301
  });
302

303
  testUsingContext('notifyingLogger outputs trace messages in verbose mode', () async {
304
    final NotifyingLogger logger = NotifyingLogger(verbose: true, parent: bufferLogger);
305 306 307

    logger.printTrace('test');

308
    expect(bufferLogger.errorText, contains('test'));
309 310 311
  });

  testUsingContext('notifyingLogger ignores trace messages in non-verbose mode', () async {
312
    final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger);
313 314 315 316 317 318 319 320 321

    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');
322
    expect(bufferLogger.errorText, contains('test'));
323 324 325
  });

  testUsingContext('notifyingLogger buffers messages sent before a subscription', () async {
326
    final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger);
327 328 329 330 331 332 333 334 335

    logger.printStatus('hello');

    final LogMessage message = await logger.onMessage.first;

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

336 337 338 339
  group('daemon serialization', () {
    test('OperationResult', () {
      expect(
        jsonEncodeObject(OperationResult.ok),
340
        '{"code":0,"message":""}',
341 342
      );
      expect(
343
        jsonEncodeObject(OperationResult(1, 'foo')),
344
        '{"code":1,"message":"foo"}',
345 346 347
      );
    });
  });
Devon Carew's avatar
Devon Carew committed
348
}
349 350

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

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

354 355 356 357 358 359 360
class MockFuchsiaWorkflow extends FuchsiaWorkflow {
  MockFuchsiaWorkflow({ this.canListDevices = true });

  @override
  final bool canListDevices;
}

361
class MockAndroidWorkflow extends AndroidWorkflow {
362
  MockAndroidWorkflow({ this.canListDevices = true });
363

364 365 366 367
  @override
  final bool canListDevices;
}

368
class MockIOSWorkflow extends IOSWorkflow {
369
  MockIOSWorkflow({ this.canListDevices = true });
370

371 372 373
  @override
  final bool canListDevices;
}