attach_test.dart 31.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
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
import 'dart:io';
7

8
import 'package:file/memory.dart';
9
import 'package:flutter_tools/src/base/common.dart';
10
import 'package:flutter_tools/src/base/dds.dart';
11
import 'package:flutter_tools/src/base/file_system.dart';
12
import 'package:flutter_tools/src/base/io.dart';
13 14
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/terminal.dart';
15 16 17
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/attach.dart';
import 'package:flutter_tools/src/device.dart';
18
import 'package:flutter_tools/src/globals.dart' as globals;
19 20
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/mdns_discovery.dart';
21
import 'package:flutter_tools/src/project.dart';
22
import 'package:flutter_tools/src/resident_runner.dart';
23
import 'package:flutter_tools/src/run_hot.dart';
24
import 'package:flutter_tools/src/vmservice.dart';
25 26 27 28
import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
29

30 31
import '../../src/common.dart';
import '../../src/context.dart';
32
import '../../src/fakes.dart';
33
import '../../src/mocks.dart';
34

35 36 37 38 39 40 41 42
final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate(
  id: '1',
  pauseEvent: vm_service.Event(
    kind: vm_service.EventKind.kResume,
    timestamp: 0
  ),
  breakpoints: <vm_service.Breakpoint>[],
  exceptionPauseMode: null,
43
  isolateFlags: <vm_service.IsolateFlag>[],
44 45 46 47 48 49 50
  libraries: <vm_service.LibraryRef>[],
  livePorts: 0,
  name: 'test',
  number: '1',
  pauseOnExit: false,
  runnable: true,
  startTime: 0,
51
  isSystemIsolate: false,
52
);
53

54 55
void main() {
  group('attach', () {
56 57
    StreamLogger logger;
    FileSystem testFileSystem;
58

59
    setUp(() {
60
      Cache.disableLocking();
61 62
      logger = StreamLogger();
      testFileSystem = MemoryFileSystem(
63
      style: globals.platform.isWindows
64 65 66
          ? FileSystemStyle.windows
          : FileSystemStyle.posix,
      );
67
      testFileSystem.directory('lib').createSync();
68
      testFileSystem.file(testFileSystem.path.join('lib', 'main.dart')).createSync();
69 70
    });

71
    group('with one device and no specified target file', () {
72 73
      const int devicePort = 499;
      const int hostPort = 42;
74

75
      FakeDeviceLogReader mockLogReader;
76
      MockPortForwarder portForwarder;
77
      MockDartDevelopmentService mockDds;
78
      MockAndroidDevice device;
79
      MockHttpClient httpClient;
80 81

      setUp(() {
82
        mockLogReader = FakeDeviceLogReader();
83 84
        portForwarder = MockPortForwarder();
        device = MockAndroidDevice();
85
        mockDds = MockDartDevelopmentService();
86 87
        when(device.portForwarder)
          .thenReturn(portForwarder);
88
        when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
89 90 91 92
          .thenAnswer((_) async => hostPort);
        when(portForwarder.forwardedPorts)
          .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
        when(portForwarder.unforward(any))
93
          .thenAnswer((_) async {});
94
        when(device.dds).thenReturn(mockDds);
95
        final Completer<void> noopCompleter = Completer<void>();
96 97
        when(mockDds.startDartDevelopmentService(any, any, false, any)).thenReturn(null);
        when(mockDds.uri).thenReturn(Uri.parse('http://localhost:8181'));
98
        when(mockDds.done).thenAnswer((_) => noopCompleter.future);
99 100 101 102 103 104 105 106
        final HttpClientRequest httpClientRequest = MockHttpClientRequest();
        httpClient = MockHttpClient();
        when(httpClient.putUrl(any))
          .thenAnswer((_) => Future<HttpClientRequest>.value(httpClientRequest));
        when(httpClientRequest.headers).thenReturn(MockHttpHeaders());
        when(httpClientRequest.close())
          .thenAnswer((_) => Future<HttpClientResponse>.value(MockHttpClientResponse()));

107 108 109 110 111
        // We cannot add the device to a device manager because that is
        // only enabled by the context of each testUsingContext call.
        //
        // Instead each test will add the device to the device manager
        // on its own.
112 113
      });

114 115 116
      tearDown(() {
        mockLogReader.dispose();
      });
117

118
      testUsingContext('finds observatory port and forwards', () async {
119 120 121 122 123 124 125
        when(device.getLogReader(includePastLogs: anyNamed('includePastLogs')))
          .thenAnswer((_) {
            // Now that the reader is used, start writing messages to it.
            mockLogReader.addLine('Foo');
            mockLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort');
            return mockLogReader;
          });
126
        testDeviceManager.addDevice(device);
127 128
        final Completer<void> completer = Completer<void>();
        final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
129 130
          if (message == '[verbose] Observatory URL on device: http://127.0.0.1:$devicePort') {
            // The "Observatory URL on device" message is output by the ProtocolDiscovery when it found the observatory.
131 132 133 134 135
            completer.complete();
          }
        });
        final Future<void> task = createTestCommandRunner(AttachCommand()).run(<String>['attach']);
        await completer.future;
136 137 138
        verify(
          portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')),
        ).called(1);
139
        await mockLogReader.dispose();
140 141
        await expectLoggerInterruptEndsTask(task, logger);
        await loggerSubscription.cancel();
142 143
      }, overrides: <Type, Generator>{
        FileSystem: () => testFileSystem,
144
        ProcessManager: () => FakeProcessManager.any(),
145
        Logger: () => logger,
146 147
      });

148
      testUsingContext('Fails with tool exit on bad Observatory uri', () async {
149 150 151 152 153 154 155 156
        when(device.getLogReader(includePastLogs: anyNamed('includePastLogs')))
          .thenAnswer((_) {
            // Now that the reader is used, start writing messages to it.
            mockLogReader.addLine('Foo');
            mockLogReader.addLine('Observatory listening on http:/:/127.0.0.1:$devicePort');
            mockLogReader.dispose();
            return mockLogReader;
          });
157 158
        testDeviceManager.addDevice(device);
        expect(createTestCommandRunner(AttachCommand()).run(<String>['attach']),
Dan Field's avatar
Dan Field committed
159
               throwsToolExit());
160 161
      }, overrides: <Type, Generator>{
        FileSystem: () => testFileSystem,
162
        ProcessManager: () => FakeProcessManager.any(),
163 164 165
        Logger: () => logger,
      });

166
      testUsingContext('accepts filesystem parameters', () async {
167 168 169 170 171 172 173
        when(device.getLogReader(includePastLogs: anyNamed('includePastLogs')))
          .thenAnswer((_) {
            // Now that the reader is used, start writing messages to it.
            mockLogReader.addLine('Foo');
            mockLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort');
            return mockLogReader;
          });
174 175 176 177 178 179 180
        testDeviceManager.addDevice(device);

        const String filesystemScheme = 'foo';
        const String filesystemRoot = '/build-output/';
        const String projectRoot = '/build-output/project-root';
        const String outputDill = '/tmp/output.dill';

181
        final MockHotRunner mockHotRunner = MockHotRunner();
182
        when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter'), allowExistingDdsInstance: true))
183
            .thenAnswer((_) async => 0);
184 185
        when(mockHotRunner.exited).thenReturn(false);
        when(mockHotRunner.isWaitingForObservatory).thenReturn(false);
186

187
        final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory();
188 189 190 191 192 193 194 195
        when(
          mockHotRunnerFactory.build(
            any,
            target: anyNamed('target'),
            projectRootPath: anyNamed('projectRootPath'),
            dillOutputPath: anyNamed('dillOutputPath'),
            debuggingOptions: anyNamed('debuggingOptions'),
            packagesFilePath: anyNamed('packagesFilePath'),
196
            flutterProject: anyNamed('flutterProject'),
197
            ipv6: false,
198
          ),
199
        ).thenReturn(mockHotRunner);
200

201
        final AttachCommand command = AttachCommand(
202 203 204 205 206 207 208 209 210 211 212 213
          hotRunnerFactory: mockHotRunnerFactory,
        );
        await createTestCommandRunner(command).run(<String>[
          'attach',
          '--filesystem-scheme',
          filesystemScheme,
          '--filesystem-root',
          filesystemRoot,
          '--project-root',
          projectRoot,
          '--output-dill',
          outputDill,
214
          '-v', // enables verbose logging
215 216 217 218 219 220 221 222 223 224 225 226
        ]);

        // Validate the attach call built a mock runner with the right
        // project root and output dill.
        final VerificationResult verificationResult = verify(
          mockHotRunnerFactory.build(
            captureAny,
            target: anyNamed('target'),
            projectRootPath: projectRoot,
            dillOutputPath: outputDill,
            debuggingOptions: anyNamed('debuggingOptions'),
            packagesFilePath: anyNamed('packagesFilePath'),
227
            flutterProject: anyNamed('flutterProject'),
228
            ipv6: false,
229 230 231
          ),
        )..called(1);

232
        final List<FlutterDevice> flutterDevices = verificationResult.captured.first as List<FlutterDevice>;
233 234 235 236 237 238 239 240 241 242
        expect(flutterDevices, hasLength(1));

        // Validate that the attach call built a flutter device with the right
        // output dill, filesystem scheme, and filesystem root.
        final FlutterDevice flutterDevice = flutterDevices.first;

        expect(flutterDevice.fileSystemScheme, filesystemScheme);
        expect(flutterDevice.fileSystemRoots, const <String>[filesystemRoot]);
      }, overrides: <Type, Generator>{
        FileSystem: () => testFileSystem,
243
        ProcessManager: () => FakeProcessManager.any(),
244
      });
245 246

      testUsingContext('exits when ipv6 is specified and debug-port is not', () async {
247 248 249 250 251 252 253
        when(device.getLogReader(includePastLogs: anyNamed('includePastLogs')))
          .thenAnswer((_) {
            // Now that the reader is used, start writing messages to it.
            mockLogReader.addLine('Foo');
            mockLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort');
            return mockLogReader;
          });
254 255 256 257 258 259
        testDeviceManager.addDevice(device);

        final AttachCommand command = AttachCommand();
        await expectLater(
          createTestCommandRunner(command).run(<String>['attach', '--ipv6']),
          throwsToolExit(
260
            message: 'When the --debug-port or --debug-uri is unknown, this command determines '
261 262 263 264 265
                     'the value of --ipv6 on its own.',
          ),
        );
      }, overrides: <Type, Generator>{
        FileSystem: () => testFileSystem,
266
        ProcessManager: () => FakeProcessManager.any(),
267 268 269
      },);

      testUsingContext('exits when observatory-port is specified and debug-port is not', () async {
270 271 272 273 274 275 276
        when(device.getLogReader(includePastLogs: anyNamed('includePastLogs')))
          .thenAnswer((_) {
            // Now that the reader is used, start writing messages to it.
            mockLogReader.addLine('Foo');
            mockLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort');
            return mockLogReader;
          });
277 278 279 280 281 282
        testDeviceManager.addDevice(device);

        final AttachCommand command = AttachCommand();
        await expectLater(
          createTestCommandRunner(command).run(<String>['attach', '--observatory-port', '100']),
          throwsToolExit(
283
            message: 'When the --debug-port or --debug-uri is unknown, this command does not use '
284 285 286 287 288
                     'the value of --observatory-port.',
          ),
        );
      }, overrides: <Type, Generator>{
        FileSystem: () => testFileSystem,
289
        ProcessManager: () => FakeProcessManager.any(),
290
      },);
291
    });
292

293 294 295
    testUsingContext('selects specified target', () async {
      const int devicePort = 499;
      const int hostPort = 42;
296
      final FakeDeviceLogReader mockLogReader = FakeDeviceLogReader();
297
      final MockPortForwarder portForwarder = MockPortForwarder();
298
      final MockDartDevelopmentService mockDds = MockDartDevelopmentService();
299
      final MockAndroidDevice device = MockAndroidDevice();
300
      final MockHotRunner mockHotRunner = MockHotRunner();
301
      final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory();
302 303
      when(device.portForwarder)
        .thenReturn(portForwarder);
304 305
      when(device.dds)
        .thenReturn(mockDds);
306 307
      final Completer<void> noopCompleter = Completer<void>();
      when(mockDds.done).thenAnswer((_) => noopCompleter.future);
308
      when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
309 310 311 312
        .thenAnswer((_) async => hostPort);
      when(portForwarder.forwardedPorts)
        .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
      when(portForwarder.unforward(any))
313
        .thenAnswer((_) async {});
314
      when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter'), allowExistingDdsInstance: true))
315
        .thenAnswer((_) async => 0);
316 317 318 319 320
      when(mockHotRunnerFactory.build(
        any,
        target: anyNamed('target'),
        debuggingOptions: anyNamed('debuggingOptions'),
        packagesFilePath: anyNamed('packagesFilePath'),
321
        flutterProject: anyNamed('flutterProject'),
322 323
        ipv6: false,
      )).thenReturn(mockHotRunner);
324 325
      when(mockHotRunner.exited).thenReturn(false);
      when(mockHotRunner.isWaitingForObservatory).thenReturn(false);
326 327
      when(mockDds.startDartDevelopmentService(any, any, false, any)).thenReturn(null);
      when(mockDds.uri).thenReturn(Uri.parse('http://localhost:8181'));
328 329

      testDeviceManager.addDevice(device);
330
      when(device.getLogReader(includePastLogs: anyNamed('includePastLogs')))
331 332
        .thenAnswer((_) {
          // Now that the reader is used, start writing messages to it.
333 334 335
          mockLogReader.addLine('Foo');
          mockLogReader.addLine(
              'Observatory listening on http://127.0.0.1:$devicePort');
336 337
          return mockLogReader;
        });
338
      final File foo = globals.fs.file('lib/foo.dart')
339 340
        ..createSync();

341
      // Delete the main.dart file to be sure that attach works without it.
342
      globals.fs.file(globals.fs.path.join('lib', 'main.dart')).deleteSync();
343

344
      final AttachCommand command = AttachCommand(hotRunnerFactory: mockHotRunnerFactory);
345 346 347 348 349 350 351
      await createTestCommandRunner(command).run(<String>[
        'attach',
        '-t',
        foo.path,
        '-v',
        '--device-user',
        '10',
352 353
        '--device-timeout',
        '15',
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
      ]);
      final VerificationResult verificationResult = verify(
        mockHotRunnerFactory.build(
          captureAny,
          target: foo.path,
          debuggingOptions: anyNamed('debuggingOptions'),
          packagesFilePath: anyNamed('packagesFilePath'),
          flutterProject: anyNamed('flutterProject'),
          ipv6: false,
        ),
      )..called(1);

      final List<FlutterDevice> flutterDevices = verificationResult.captured.first as List<FlutterDevice>;
      expect(flutterDevices, hasLength(1));
      final FlutterDevice flutterDevice = flutterDevices.first;
      expect(flutterDevice.userIdentifier, '10');
370 371
    }, overrides: <Type, Generator>{
      FileSystem: () => testFileSystem,
372
      ProcessManager: () => FakeProcessManager.any(),
373
    });
374

375 376 377
    testUsingContext('fallbacks to protocol observatory if MDNS failed on iOS', () async {
      const int devicePort = 499;
      const int hostPort = 42;
378
      final FakeDeviceLogReader mockLogReader = FakeDeviceLogReader();
379
      final MockPortForwarder portForwarder = MockPortForwarder();
380
      final MockDartDevelopmentService mockDds = MockDartDevelopmentService();
381 382 383
      final MockIOSDevice device = MockIOSDevice();
      final MockHotRunner mockHotRunner = MockHotRunner();
      final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory();
384 385 386 387
      when(device.portForwarder)
        .thenReturn(portForwarder);
      when(device.dds)
        .thenReturn(mockDds);
388 389
      final Completer<void> noopCompleter = Completer<void>();
      when(mockDds.done).thenAnswer((_) => noopCompleter.future);
390 391
      when(device.getLogReader(includePastLogs: anyNamed('includePastLogs')))
        .thenAnswer((_) => mockLogReader);
392 393 394 395 396
      when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
        .thenAnswer((_) async => hostPort);
      when(portForwarder.forwardedPorts)
        .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
      when(portForwarder.unforward(any))
397
        .thenAnswer((_) async {});
398
      when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter'), allowExistingDdsInstance: true))
399 400 401 402 403 404 405 406 407 408 409
        .thenAnswer((_) async => 0);
      when(mockHotRunnerFactory.build(
        any,
        target: anyNamed('target'),
        debuggingOptions: anyNamed('debuggingOptions'),
        packagesFilePath: anyNamed('packagesFilePath'),
        flutterProject: anyNamed('flutterProject'),
        ipv6: false,
      )).thenReturn(mockHotRunner);
      when(mockHotRunner.exited).thenReturn(false);
      when(mockHotRunner.isWaitingForObservatory).thenReturn(false);
410 411
      when(mockDds.startDartDevelopmentService(any, any, false, any)).thenReturn(null);
      when(mockDds.uri).thenReturn(Uri.parse('http://localhost:8181'));
412 413 414

      testDeviceManager.addDevice(device);

415
      final File foo = globals.fs.file('lib/foo.dart')..createSync();
416 417

      // Delete the main.dart file to be sure that attach works without it.
418
      globals.fs.file(globals.fs.path.join('lib', 'main.dart')).deleteSync();
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433

      final AttachCommand command = AttachCommand(hotRunnerFactory: mockHotRunnerFactory);
      await createTestCommandRunner(command).run(<String>['attach', '-t', foo.path, '-v']);

      verify(mockHotRunnerFactory.build(
        any,
        target: foo.path,
        debuggingOptions: anyNamed('debuggingOptions'),
        packagesFilePath: anyNamed('packagesFilePath'),
        flutterProject: anyNamed('flutterProject'),
        ipv6: false,
      )).called(1);
    }, overrides: <Type, Generator>{
      FileSystem: () => testFileSystem,
      ProcessManager: () => FakeProcessManager.any(),
434
    }, skip: Platform.isWindows); // mDNS does not work on Windows.
435

436
    group('forwarding to given port', () {
437 438
      const int devicePort = 499;
      const int hostPort = 42;
439 440
      MockPortForwarder portForwarder;
      MockAndroidDevice device;
441

442 443
      setUp(() {
        portForwarder = MockPortForwarder();
444
        final MockDartDevelopmentService mockDds = MockDartDevelopmentService();
445
        device = MockAndroidDevice();
446

447 448 449 450 451 452 453
        when(device.portForwarder)
          .thenReturn(portForwarder);
        when(portForwarder.forward(devicePort))
          .thenAnswer((_) async => hostPort);
        when(portForwarder.forwardedPorts)
          .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
        when(portForwarder.unforward(any))
454
          .thenAnswer((_) async {});
455 456
        when(device.dds)
          .thenReturn(mockDds);
457
        when(mockDds.startDartDevelopmentService(any, any, any, any))
458
          .thenReturn(null);
459
        when(mockDds.uri).thenReturn(Uri.parse('http://localhost:8181'));
460 461
        final Completer<void> noopCompleter = Completer<void>();
        when(mockDds.done).thenAnswer((_) => noopCompleter.future);
462
      });
463

464 465
      testUsingContext('succeeds in ipv4 mode', () async {
        testDeviceManager.addDevice(device);
466

467 468 469 470 471 472 473 474 475 476 477
        final Completer<void> completer = Completer<void>();
        final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
          if (message == '[verbose] Connecting to service protocol: http://127.0.0.1:42/') {
            // Wait until resident_runner.dart tries to connect.
            // There's nothing to connect _to_, so that's as far as we care to go.
            completer.complete();
          }
        });
        final Future<void> task = createTestCommandRunner(AttachCommand())
          .run(<String>['attach', '--debug-port', '$devicePort']);
        await completer.future;
478
        verify(portForwarder.forward(devicePort)).called(1);
479 480 481

        await expectLoggerInterruptEndsTask(task, logger);
        await loggerSubscription.cancel();
482 483
      }, overrides: <Type, Generator>{
        FileSystem: () => testFileSystem,
484
        ProcessManager: () => FakeProcessManager.any(),
485
        Logger: () => logger,
486 487 488 489
      });

      testUsingContext('succeeds in ipv6 mode', () async {
        testDeviceManager.addDevice(device);
490

491 492 493 494 495 496 497 498 499 500 501
        final Completer<void> completer = Completer<void>();
        final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
          if (message == '[verbose] Connecting to service protocol: http://[::1]:42/') {
            // Wait until resident_runner.dart tries to connect.
            // There's nothing to connect _to_, so that's as far as we care to go.
            completer.complete();
          }
        });
        final Future<void> task = createTestCommandRunner(AttachCommand())
          .run(<String>['attach', '--debug-port', '$devicePort', '--ipv6']);
        await completer.future;
502
        verify(portForwarder.forward(devicePort)).called(1);
503 504 505

        await expectLoggerInterruptEndsTask(task, logger);
        await loggerSubscription.cancel();
506 507
      }, overrides: <Type, Generator>{
        FileSystem: () => testFileSystem,
508
        ProcessManager: () => FakeProcessManager.any(),
509
        Logger: () => logger,
510 511 512 513 514
      });

      testUsingContext('skips in ipv4 mode with a provided observatory port', () async {
        testDeviceManager.addDevice(device);

515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
        final Completer<void> completer = Completer<void>();
        final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
          if (message == '[verbose] Connecting to service protocol: http://127.0.0.1:42/') {
            // Wait until resident_runner.dart tries to connect.
            // There's nothing to connect _to_, so that's as far as we care to go.
            completer.complete();
          }
        });
        final Future<void> task = createTestCommandRunner(AttachCommand()).run(
          <String>[
            'attach',
            '--debug-port',
            '$devicePort',
            '--observatory-port',
            '$hostPort',
530 531 532
            // Ensure DDS doesn't use hostPort by binding to a random port.
            '--dds-port',
            '0',
533
          ],
534
        );
535
        await completer.future;
536
        verifyNever(portForwarder.forward(devicePort));
537 538 539

        await expectLoggerInterruptEndsTask(task, logger);
        await loggerSubscription.cancel();
540 541
      }, overrides: <Type, Generator>{
        FileSystem: () => testFileSystem,
542
        ProcessManager: () => FakeProcessManager.any(),
543
        Logger: () => logger,
544 545 546 547 548
      });

      testUsingContext('skips in ipv6 mode with a provided observatory port', () async {
        testDeviceManager.addDevice(device);

549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564
        final Completer<void> completer = Completer<void>();
        final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
          if (message == '[verbose] Connecting to service protocol: http://[::1]:42/') {
            // Wait until resident_runner.dart tries to connect.
            // There's nothing to connect _to_, so that's as far as we care to go.
            completer.complete();
          }
        });
        final Future<void> task = createTestCommandRunner(AttachCommand()).run(
          <String>[
            'attach',
            '--debug-port',
            '$devicePort',
            '--observatory-port',
            '$hostPort',
            '--ipv6',
565 566 567
            // Ensure DDS doesn't use hostPort by binding to a random port.
            '--dds-port',
            '0',
568
          ],
569
        );
570
        await completer.future;
571
        verifyNever(portForwarder.forward(devicePort));
572 573 574

        await expectLoggerInterruptEndsTask(task, logger);
        await loggerSubscription.cancel();
575 576
      }, overrides: <Type, Generator>{
        FileSystem: () => testFileSystem,
577
        ProcessManager: () => FakeProcessManager.any(),
578
        Logger: () => logger,
579 580
      });
    });
581 582

    testUsingContext('exits when no device connected', () async {
583
      final AttachCommand command = AttachCommand();
584 585
      await expectLater(
        createTestCommandRunner(command).run(<String>['attach']),
Dan Field's avatar
Dan Field committed
586
        throwsToolExit(),
587
      );
588
      expect(testLogger.statusText, containsIgnoringWhitespace('No supported devices connected'));
589 590
    }, overrides: <Type, Generator>{
      FileSystem: () => testFileSystem,
591
      ProcessManager: () => FakeProcessManager.any(),
592
    });
593

594 595 596 597 598 599 600 601 602 603 604 605 606
    testUsingContext('fails when targeted device is not Android with --device-user', () async {
      final MockIOSDevice device = MockIOSDevice();
      testDeviceManager.addDevice(device);
      expect(createTestCommandRunner(AttachCommand()).run(<String>[
        'attach',
        '--device-user',
        '10',
      ]), throwsToolExit(message: '--device-user is only supported for Android'));
    }, overrides: <Type, Generator>{
      FileSystem: () => testFileSystem,
      ProcessManager: () => FakeProcessManager.any(),
    });

607 608
    testUsingContext('exits when multiple devices connected', () async {
      Device aDeviceWithId(String id) {
609
        final MockAndroidDevice device = MockAndroidDevice();
610 611 612 613
        when(device.name).thenReturn('d$id');
        when(device.id).thenReturn(id);
        when(device.isLocalEmulator).thenAnswer((_) async => false);
        when(device.sdkNameAndVersion).thenAnswer((_) async => 'Android 46');
614 615 616
        when(device.targetPlatformDisplayName)
            .thenAnswer((_) async => 'android');

617 618 619
        return device;
      }

620
      final AttachCommand command = AttachCommand();
621 622 623 624
      testDeviceManager.addDevice(aDeviceWithId('xx1'));
      testDeviceManager.addDevice(aDeviceWithId('yy2'));
      await expectLater(
        createTestCommandRunner(command).run(<String>['attach']),
Dan Field's avatar
Dan Field committed
625
        throwsToolExit(),
626
      );
627
      expect(testLogger.statusText, containsIgnoringWhitespace('More than one device'));
628 629
      expect(testLogger.statusText, contains('xx1'));
      expect(testLogger.statusText, contains('yy2'));
630 631
    }, overrides: <Type, Generator>{
      FileSystem: () => testFileSystem,
632
      ProcessManager: () => FakeProcessManager.any(),
633
    });
634 635 636
  });
}

637 638
class MockHotRunner extends Mock implements HotRunner {}
class MockHotRunnerFactory extends Mock implements HotRunnerFactory {}
639 640 641
class MockIOSDevice extends Mock implements IOSDevice {}
class MockMDnsObservatoryDiscovery extends Mock implements MDnsObservatoryDiscovery {}
class MockPortForwarder extends Mock implements DevicePortForwarder {}
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686

class StreamLogger extends Logger {
  @override
  bool get isVerbose => true;

  @override
  void printError(
    String message, {
    StackTrace stackTrace,
    bool emphasis,
    TerminalColor color,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
    _log('[stderr] $message');
  }

  @override
  void printStatus(
    String message, {
    bool emphasis,
    TerminalColor color,
    bool newline,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
    _log('[stdout] $message');
  }

  @override
  void printTrace(String message) {
    _log('[verbose] $message');
  }

  @override
  Status startProgress(
    String message, {
    @required Duration timeout,
    String progressId,
    bool multilineOutput = false,
    int progressIndicatorPadding = kDefaultStatusPadding,
  }) {
    _log('[progress] $message');
687 688 689
    return SilentStatus(
      stopwatch: Stopwatch(),
    )..start();
690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708
  }

  bool _interrupt = false;

  void interrupt() {
    _interrupt = true;
  }

  final StreamController<String> _controller = StreamController<String>.broadcast();

  void _log(String message) {
    _controller.add(message);
    if (_interrupt) {
      _interrupt = false;
      throw const LoggerInterrupted();
    }
  }

  Stream<String> get stream => _controller.stream;
709 710

  @override
711
  void sendEvent(String name, [Map<String, dynamic> args]) { }
712 713 714 715 716 717

  @override
  bool get supportsColor => throw UnimplementedError();

  @override
  bool get hasTerminal => false;
718 719 720

  @override
  void clear() => _log('[stdout] ${globals.terminal.clearScreen()}\n');
721 722 723 724 725 726 727 728 729 730 731 732 733 734 735
}

class LoggerInterrupted implements Exception {
  const LoggerInterrupted();
}

Future<void> expectLoggerInterruptEndsTask(Future<void> task, StreamLogger logger) async {
  logger.interrupt(); // an exception during the task should cause it to fail...
  try {
    await task;
    expect(false, isTrue); // (shouldn't reach here)
  } on ToolExit catch (error) {
    expect(error.exitCode, 2); // ...with exit code 2.
  }
}
736 737 738 739 740 741 742 743 744 745 746

VMServiceConnector getFakeVmServiceFactory({
  @required Completer<void> vmServiceDoneCompleter,
}) {
  assert(vmServiceDoneCompleter != null);

  return (
    Uri httpUri, {
    ReloadSources reloadSources,
    Restart restart,
    CompileExpression compileExpression,
747
    GetSkSLMethod getSkSLMethod,
748
    PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
749
    CompressionOptions compression,
750
    Device device,
751
  }) async {
752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
      requests: <VmServiceExpectation>[
        FakeVmServiceRequest(
          method: kListViewsMethod,
          args: null,
          jsonResponse: <String, Object>{
            'views': <Object>[
              <String, Object>{
                'id': '1',
                'isolate': fakeUnpausedIsolate.toJson()
              },
            ],
          },
        ),
        FakeVmServiceRequest(
          method: 'getVM',
          args: null,
          jsonResponse: vm_service.VM.parse(<String, Object>{})
            .toJson(),
        ),
        FakeVmServiceRequest(
          method: '_createDevFS',
          args: <String, Object>{
            'fsName': globals.fs.currentDirectory.absolute.path,
          },
          jsonResponse: <String, Object>{
            'uri': globals.fs.currentDirectory.absolute.path,
          },
        ),
        FakeVmServiceRequest(
          method: kListViewsMethod,
          args: null,
          jsonResponse: <String, Object>{
            'views': <Object>[
              <String, Object>{
                'id': '1',
                'isolate': fakeUnpausedIsolate.toJson()
              },
            ],
          },
        ),
      ],
    );
    return fakeVmServiceHost.vmService;
796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837
  };
}

class TestHotRunnerFactory extends HotRunnerFactory {
  HotRunner _runner;

  @override
  HotRunner build(
    List<FlutterDevice> devices, {
    String target,
    DebuggingOptions debuggingOptions,
    bool benchmarkMode = false,
    File applicationBinary,
    bool hostIsIde = false,
    String projectRootPath,
    String packagesFilePath,
    String dillOutputPath,
    bool stayResident = true,
    bool ipv6 = false,
    FlutterProject flutterProject,
  }) {
    _runner ??= HotRunner(
      devices,
      target: target,
      debuggingOptions: debuggingOptions,
      benchmarkMode: benchmarkMode,
      applicationBinary: applicationBinary,
      hostIsIde: hostIsIde,
      projectRootPath: projectRootPath,
      dillOutputPath: dillOutputPath,
      stayResident: stayResident,
      ipv6: ipv6,
    );
    return _runner;
  }

  Future<void> exitApp() async {
    assert(_runner != null);
    await _runner.exit();
  }
}

838
class MockDartDevelopmentService extends Mock implements DartDevelopmentService {}
839 840
class MockHttpClientRequest extends Mock implements HttpClientRequest {}
class MockHttpClientResponse extends Mock implements HttpClientResponse {}
841
class MockHttpHeaders extends Mock implements HttpHeaders {}