daemon_test.dart 13.7 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 24 25
  Daemon daemon;
  NotifyingLogger notifyingLogger;

  group('daemon', () {
26
    setUp(() {
27
      notifyingLogger = NotifyingLogger();
28
    });
Devon Carew's avatar
Devon Carew committed
29 30

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

37
    testUsingContext('daemon.version command should succeed', () async {
38 39 40
      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
41
        commands.stream,
42
        responses.add,
43
        notifyingLogger: notifyingLogger,
44
        dartDefines: const <String>[],
Devon Carew's avatar
Devon Carew committed
45
      );
Ian Hickson's avatar
Ian Hickson committed
46
      commands.add(<String, dynamic>{'id': 0, 'method': 'daemon.version'});
47
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
Devon Carew's avatar
Devon Carew committed
48 49 50
      expect(response['id'], 0);
      expect(response['result'], isNotEmpty);
      expect(response['result'] is String, true);
51 52
      await responses.close();
      await commands.close();
Devon Carew's avatar
Devon Carew committed
53 54
    });

55
    testUsingContext('printError should send daemon.logMessage event', () async {
56 57 58
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
59 60 61
        commands.stream,
        responses.add,
        notifyingLogger: notifyingLogger,
62
        dartDefines: const <String>[],
63
      );
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
          commands.stream,
          responses.add,
          notifyingLogger: notifyingLogger,
          logToStdout: true,
90
          dartDefines: const <String>[],
91
        );
92
        globals.printStatus('daemon.logMessage test');
93
        // Service the event loop.
94
        await Future<void>.value();
95
      }, zoneSpecification: ZoneSpecification(print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
96 97 98 99
        buffer.writeln(line);
      }));

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

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

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

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

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

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

162
    testUsingContext('app.stop without appId should report an error', () async {
163 164 165
      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
166
        commands.stream,
167
        responses.add,
168
        notifyingLogger: notifyingLogger,
169
        dartDefines: const <String>[],
Devon Carew's avatar
Devon Carew committed
170 171
      );

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

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

Chris Bracken's avatar
Chris Bracken committed
197
    testUsingContext('device.getDevices reports available devices', () async {
198 199 200 201 202 203
      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,
204
        dartDefines: const <String>[],
205 206 207 208 209 210 211 212 213 214 215 216 217 218
      );
      final MockPollingDeviceDiscovery discoverer = MockPollingDeviceDiscovery();
      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();
    });

219
    testUsingContext('should send device.added event when device is discovered', () async {
220 221 222
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
223 224 225 226
        commands.stream,
        responses.add,
        notifyingLogger: notifyingLogger,
        dartDefines: const <String>[],
227 228
      );

229
      final MockPollingDeviceDiscovery discoverer = MockPollingDeviceDiscovery();
230
      daemon.deviceDomain.addDeviceDiscoverer(discoverer);
231
      discoverer.addDevice(MockAndroidDevice());
232

233
      return await responses.stream.skipWhile(_isConnectedEvent).first.then<void>((Map<String, dynamic> response) async {
234 235 236
        expect(response['event'], 'device.added');
        expect(response['params'], isMap);

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

240 241
        await responses.close();
        await commands.close();
242
      });
243
    }, overrides: <Type, Generator>{
244 245
      AndroidWorkflow: () => MockAndroidWorkflow(),
      IOSWorkflow: () => MockIOSWorkflow(),
246
      FuchsiaWorkflow: () => MockFuchsiaWorkflow(),
247
    });
248 249

    testUsingContext('emulator.launch without an emulatorId should report an error', () async {
250 251 252
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
253 254
        commands.stream,
        responses.add,
255
        notifyingLogger: notifyingLogger,
256
        dartDefines: const <String>[],
257 258
      );

259
      commands.add(<String, dynamic>{'id': 0, 'method': 'emulator.launch'});
260 261 262
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
      expect(response['id'], 0);
      expect(response['error'], contains('emulatorId is required'));
263 264
      await responses.close();
      await commands.close();
265 266 267
    });

    testUsingContext('emulator.getEmulators should respond with list', () async {
268 269 270
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
271 272
        commands.stream,
        responses.add,
273
        notifyingLogger: notifyingLogger,
274
        dartDefines: const <String>[],
275 276 277 278 279
      );
      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);
280 281
      await responses.close();
      await commands.close();
282
    });
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

    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,
        dartDefines: const <String>[],
      );

      // 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
312
  });
313 314 315 316 317

  group('daemon serialization', () {
    test('OperationResult', () {
      expect(
        jsonEncodeObject(OperationResult.ok),
318
        '{"code":0,"message":""}',
319 320
      );
      expect(
321
        jsonEncodeObject(OperationResult(1, 'foo')),
322
        '{"code":1,"message":"foo"}',
323 324 325
      );
    });
  });
Devon Carew's avatar
Devon Carew committed
326
}
327 328

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

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

332 333 334 335 336 337 338
class MockFuchsiaWorkflow extends FuchsiaWorkflow {
  MockFuchsiaWorkflow({ this.canListDevices = true });

  @override
  final bool canListDevices;
}

339
class MockAndroidWorkflow extends AndroidWorkflow {
340
  MockAndroidWorkflow({ this.canListDevices = true });
341

342 343 344 345
  @override
  final bool canListDevices;
}

346
class MockIOSWorkflow extends IOSWorkflow {
347
  MockIOSWorkflow({ this.canListDevices = true });
348

349 350 351
  @override
  final bool canListDevices;
}