daemon_test.dart 13.2 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,
Devon Carew's avatar
Devon Carew committed
44
      );
Ian Hickson's avatar
Ian Hickson committed
45
      commands.add(<String, dynamic>{'id': 0, 'method': 'daemon.version'});
46
      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
Devon Carew's avatar
Devon Carew committed
47 48
      expect(response['id'], 0);
      expect(response['result'], isNotEmpty);
Dan Field's avatar
Dan Field committed
49
      expect(response['result'], isA<String>());
50 51
      await responses.close();
      await commands.close();
Devon Carew's avatar
Devon Carew committed
52 53
    });

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

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

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

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

101
    testUsingContext('daemon.shutdown command should stop daemon', () async {
102 103 104
      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
105
        commands.stream,
106
        responses.add,
107
        notifyingLogger: notifyingLogger,
Devon Carew's avatar
Devon Carew committed
108
      );
109
      commands.add(<String, dynamic>{'id': 0, 'method': 'daemon.shutdown'});
110
      return daemon.onExit.then<void>((int code) async {
111
        await commands.close();
Devon Carew's avatar
Devon Carew committed
112 113 114 115
        expect(code, 0);
      });
    });

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

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

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

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

156
    testUsingContext('app.stop without appId should report an error', () async {
157 158 159
      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
160
        commands.stream,
161
        responses.add,
162
        notifyingLogger: notifyingLogger,
Devon Carew's avatar
Devon Carew committed
163 164
      );

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

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

Chris Bracken's avatar
Chris Bracken committed
189
    testUsingContext('device.getDevices reports available devices', () async {
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
      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,
      );
      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();
    });

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

219
      final MockPollingDeviceDiscovery discoverer = MockPollingDeviceDiscovery();
220
      daemon.deviceDomain.addDeviceDiscoverer(discoverer);
221
      discoverer.addDevice(MockAndroidDevice());
222

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

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

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

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

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

    testUsingContext('emulator.getEmulators should respond with list', () async {
257 258 259
      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
      daemon = Daemon(
260 261
        commands.stream,
        responses.add,
262
        notifyingLogger: notifyingLogger,
263 264 265 266 267
      );
      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);
268 269
      await responses.close();
      await commands.close();
270
    });
271 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

    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
299
  });
300 301 302 303 304

  group('daemon serialization', () {
    test('OperationResult', () {
      expect(
        jsonEncodeObject(OperationResult.ok),
305
        '{"code":0,"message":""}',
306 307
      );
      expect(
308
        jsonEncodeObject(OperationResult(1, 'foo')),
309
        '{"code":1,"message":"foo"}',
310 311 312
      );
    });
  });
Devon Carew's avatar
Devon Carew committed
313
}
314 315

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

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

319 320 321 322 323 324 325
class MockFuchsiaWorkflow extends FuchsiaWorkflow {
  MockFuchsiaWorkflow({ this.canListDevices = true });

  @override
  final bool canListDevices;
}

326
class MockAndroidWorkflow extends AndroidWorkflow {
327
  MockAndroidWorkflow({ this.canListDevices = true });
328

329 330 331 332
  @override
  final bool canListDevices;
}

333
class MockIOSWorkflow extends IOSWorkflow {
334
  MockIOSWorkflow({ this.canListDevices = true });
335

336 337 338
  @override
  final bool canListDevices;
}