attach_test.dart 29.2 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/file_system.dart';
11
import 'package:flutter_tools/src/base/io.dart';
12 13
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/terminal.dart';
14 15 16
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/attach.dart';
import 'package:flutter_tools/src/device.dart';
17
import 'package:flutter_tools/src/globals.dart' as globals;
18 19
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/mdns_discovery.dart';
20
import 'package:flutter_tools/src/project.dart';
21
import 'package:flutter_tools/src/resident_runner.dart';
22
import 'package:flutter_tools/src/run_hot.dart';
23
import 'package:flutter_tools/src/vmservice.dart';
24 25 26 27
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;
28

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

34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
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,
  libraries: <vm_service.LibraryRef>[],
  livePorts: 0,
  name: 'test',
  number: '1',
  pauseOnExit: false,
  runnable: true,
  startTime: 0,
);
50

51 52
void main() {
  group('attach', () {
53 54
    StreamLogger logger;
    FileSystem testFileSystem;
55

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

68
    group('with one device and no specified target file', () {
69 70
      const int devicePort = 499;
      const int hostPort = 42;
71

72
      FakeDeviceLogReader mockLogReader;
73 74
      MockPortForwarder portForwarder;
      MockAndroidDevice device;
75
      MockHttpClient httpClient;
76 77

      setUp(() {
78
        mockLogReader = FakeDeviceLogReader();
79 80
        portForwarder = MockPortForwarder();
        device = MockAndroidDevice();
81 82
        when(device.portForwarder)
          .thenReturn(portForwarder);
83
        when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
84 85 86 87 88
          .thenAnswer((_) async => hostPort);
        when(portForwarder.forwardedPorts)
          .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
        when(portForwarder.unforward(any))
          .thenAnswer((_) async => null);
89

90 91 92 93 94 95 96 97
        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()));

98 99 100 101 102
        // 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.
103 104
      });

105 106 107
      tearDown(() {
        mockLogReader.dispose();
      });
108

109
      testUsingContext('finds observatory port and forwards', () async {
110 111 112 113 114 115 116
        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;
          });
117
        testDeviceManager.addDevice(device);
118 119
        final Completer<void> completer = Completer<void>();
        final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
120 121
          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.
122 123 124 125 126
            completer.complete();
          }
        });
        final Future<void> task = createTestCommandRunner(AttachCommand()).run(<String>['attach']);
        await completer.future;
127 128 129
        verify(
          portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')),
        ).called(1);
130
        await mockLogReader.dispose();
131 132
        await expectLoggerInterruptEndsTask(task, logger);
        await loggerSubscription.cancel();
133 134
      }, overrides: <Type, Generator>{
        FileSystem: () => testFileSystem,
135
        ProcessManager: () => FakeProcessManager.any(),
136
        Logger: () => logger,
137 138
      });

139
      testUsingContext('Fails with tool exit on bad Observatory uri', () async {
140 141 142 143 144 145 146 147
        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;
          });
148 149
        testDeviceManager.addDevice(device);
        expect(createTestCommandRunner(AttachCommand()).run(<String>['attach']),
Dan Field's avatar
Dan Field committed
150
               throwsToolExit());
151 152
      }, overrides: <Type, Generator>{
        FileSystem: () => testFileSystem,
153
        ProcessManager: () => FakeProcessManager.any(),
154 155 156
        Logger: () => logger,
      });

157
      testUsingContext('accepts filesystem parameters', () async {
158 159 160 161 162 163 164
        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;
          });
165 166 167 168 169 170 171
        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';

172
        final MockHotRunner mockHotRunner = MockHotRunner();
173 174
        when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter')))
            .thenAnswer((_) async => 0);
175 176
        when(mockHotRunner.exited).thenReturn(false);
        when(mockHotRunner.isWaitingForObservatory).thenReturn(false);
177

178
        final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory();
179 180 181 182 183 184 185 186
        when(
          mockHotRunnerFactory.build(
            any,
            target: anyNamed('target'),
            projectRootPath: anyNamed('projectRootPath'),
            dillOutputPath: anyNamed('dillOutputPath'),
            debuggingOptions: anyNamed('debuggingOptions'),
            packagesFilePath: anyNamed('packagesFilePath'),
187
            flutterProject: anyNamed('flutterProject'),
188
            ipv6: false,
189
          ),
190
        ).thenReturn(mockHotRunner);
191

192
        final AttachCommand command = AttachCommand(
193 194 195 196 197 198 199 200 201 202 203 204
          hotRunnerFactory: mockHotRunnerFactory,
        );
        await createTestCommandRunner(command).run(<String>[
          'attach',
          '--filesystem-scheme',
          filesystemScheme,
          '--filesystem-root',
          filesystemRoot,
          '--project-root',
          projectRoot,
          '--output-dill',
          outputDill,
205
          '-v', // enables verbose logging
206 207 208 209 210 211 212 213 214 215 216 217
        ]);

        // 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'),
218
            flutterProject: anyNamed('flutterProject'),
219
            ipv6: false,
220 221 222
          ),
        )..called(1);

223
        final List<FlutterDevice> flutterDevices = verificationResult.captured.first as List<FlutterDevice>;
224 225 226 227 228 229 230 231 232 233
        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,
234
        ProcessManager: () => FakeProcessManager.any(),
235
      });
236 237

      testUsingContext('exits when ipv6 is specified and debug-port is not', () async {
238 239 240 241 242 243 244
        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;
          });
245 246 247 248 249 250
        testDeviceManager.addDevice(device);

        final AttachCommand command = AttachCommand();
        await expectLater(
          createTestCommandRunner(command).run(<String>['attach', '--ipv6']),
          throwsToolExit(
251
            message: 'When the --debug-port or --debug-uri is unknown, this command determines '
252 253 254 255 256
                     'the value of --ipv6 on its own.',
          ),
        );
      }, overrides: <Type, Generator>{
        FileSystem: () => testFileSystem,
257
        ProcessManager: () => FakeProcessManager.any(),
258 259 260
      },);

      testUsingContext('exits when observatory-port is specified and debug-port is not', () async {
261 262 263 264 265 266 267
        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;
          });
268 269 270 271 272 273
        testDeviceManager.addDevice(device);

        final AttachCommand command = AttachCommand();
        await expectLater(
          createTestCommandRunner(command).run(<String>['attach', '--observatory-port', '100']),
          throwsToolExit(
274
            message: 'When the --debug-port or --debug-uri is unknown, this command does not use '
275 276 277 278 279
                     'the value of --observatory-port.',
          ),
        );
      }, overrides: <Type, Generator>{
        FileSystem: () => testFileSystem,
280
        ProcessManager: () => FakeProcessManager.any(),
281
      },);
282
    });
283

284 285 286
    testUsingContext('selects specified target', () async {
      const int devicePort = 499;
      const int hostPort = 42;
287
      final FakeDeviceLogReader mockLogReader = FakeDeviceLogReader();
288 289
      final MockPortForwarder portForwarder = MockPortForwarder();
      final MockAndroidDevice device = MockAndroidDevice();
290
      final MockHotRunner mockHotRunner = MockHotRunner();
291
      final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory();
292 293
      when(device.portForwarder)
        .thenReturn(portForwarder);
294
      when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
295 296 297 298 299
        .thenAnswer((_) async => hostPort);
      when(portForwarder.forwardedPorts)
        .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
      when(portForwarder.unforward(any))
        .thenAnswer((_) async => null);
300
      when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter')))
301
        .thenAnswer((_) async => 0);
302 303 304 305 306
      when(mockHotRunnerFactory.build(
        any,
        target: anyNamed('target'),
        debuggingOptions: anyNamed('debuggingOptions'),
        packagesFilePath: anyNamed('packagesFilePath'),
307
        flutterProject: anyNamed('flutterProject'),
308 309
        ipv6: false,
      )).thenReturn(mockHotRunner);
310 311
      when(mockHotRunner.exited).thenReturn(false);
      when(mockHotRunner.isWaitingForObservatory).thenReturn(false);
312 313

      testDeviceManager.addDevice(device);
314
      when(device.getLogReader(includePastLogs: anyNamed('includePastLogs')))
315 316
        .thenAnswer((_) {
          // Now that the reader is used, start writing messages to it.
317 318 319
          mockLogReader.addLine('Foo');
          mockLogReader.addLine(
              'Observatory listening on http://127.0.0.1:$devicePort');
320 321
          return mockLogReader;
        });
322
      final File foo = globals.fs.file('lib/foo.dart')
323 324
        ..createSync();

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

328
      final AttachCommand command = AttachCommand(hotRunnerFactory: mockHotRunnerFactory);
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351
      await createTestCommandRunner(command).run(<String>[
        'attach',
        '-t',
        foo.path,
        '-v',
        '--device-user',
        '10',
      ]);
      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');
352 353
    }, overrides: <Type, Generator>{
      FileSystem: () => testFileSystem,
354
      ProcessManager: () => FakeProcessManager.any(),
355
    });
356

357 358 359
    testUsingContext('fallbacks to protocol observatory if MDNS failed on iOS', () async {
      const int devicePort = 499;
      const int hostPort = 42;
360
      final FakeDeviceLogReader mockLogReader = FakeDeviceLogReader();
361 362 363 364 365
      final MockPortForwarder portForwarder = MockPortForwarder();
      final MockIOSDevice device = MockIOSDevice();
      final MockHotRunner mockHotRunner = MockHotRunner();
      final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory();
      when(device.portForwarder).thenReturn(portForwarder);
366 367
      when(device.getLogReader(includePastLogs: anyNamed('includePastLogs')))
        .thenAnswer((_) => mockLogReader);
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
      when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
        .thenAnswer((_) async => hostPort);
      when(portForwarder.forwardedPorts)
        .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
      when(portForwarder.unforward(any))
        .thenAnswer((_) async => null);
      when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter')))
        .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);

      testDeviceManager.addDevice(device);

389
      final File foo = globals.fs.file('lib/foo.dart')..createSync();
390 391

      // Delete the main.dart file to be sure that attach works without it.
392
      globals.fs.file(globals.fs.path.join('lib', 'main.dart')).deleteSync();
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407

      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(),
408
    }, skip: Platform.isWindows); // mDNS does not work on Windows.
409

410
    group('forwarding to given port', () {
411 412
      const int devicePort = 499;
      const int hostPort = 42;
413 414
      MockPortForwarder portForwarder;
      MockAndroidDevice device;
415

416 417 418
      setUp(() {
        portForwarder = MockPortForwarder();
        device = MockAndroidDevice();
419

420 421 422 423 424 425 426 427
        when(device.portForwarder)
          .thenReturn(portForwarder);
        when(portForwarder.forward(devicePort))
          .thenAnswer((_) async => hostPort);
        when(portForwarder.forwardedPorts)
          .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
        when(portForwarder.unforward(any))
          .thenAnswer((_) async => null);
428
      });
429

430 431
      testUsingContext('succeeds in ipv4 mode', () async {
        testDeviceManager.addDevice(device);
432

433 434 435 436 437 438 439 440 441 442 443
        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;
444
        verify(portForwarder.forward(devicePort)).called(1);
445 446 447

        await expectLoggerInterruptEndsTask(task, logger);
        await loggerSubscription.cancel();
448 449
      }, overrides: <Type, Generator>{
        FileSystem: () => testFileSystem,
450
        ProcessManager: () => FakeProcessManager.any(),
451
        Logger: () => logger,
452 453 454 455
      });

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

457 458 459 460 461 462 463 464 465 466 467
        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;
468
        verify(portForwarder.forward(devicePort)).called(1);
469 470 471

        await expectLoggerInterruptEndsTask(task, logger);
        await loggerSubscription.cancel();
472 473
      }, overrides: <Type, Generator>{
        FileSystem: () => testFileSystem,
474
        ProcessManager: () => FakeProcessManager.any(),
475
        Logger: () => logger,
476 477 478 479 480
      });

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

481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
        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',
          ],
497
        );
498
        await completer.future;
499
        verifyNever(portForwarder.forward(devicePort));
500 501 502

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

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

512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528
        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',
          ],
529
        );
530
        await completer.future;
531
        verifyNever(portForwarder.forward(devicePort));
532 533 534

        await expectLoggerInterruptEndsTask(task, logger);
        await loggerSubscription.cancel();
535 536
      }, overrides: <Type, Generator>{
        FileSystem: () => testFileSystem,
537
        ProcessManager: () => FakeProcessManager.any(),
538
        Logger: () => logger,
539 540
      });
    });
541 542

    testUsingContext('exits when no device connected', () async {
543
      final AttachCommand command = AttachCommand();
544 545
      await expectLater(
        createTestCommandRunner(command).run(<String>['attach']),
Dan Field's avatar
Dan Field committed
546
        throwsToolExit(),
547
      );
548
      expect(testLogger.statusText, containsIgnoringWhitespace('No supported devices connected'));
549 550
    }, overrides: <Type, Generator>{
      FileSystem: () => testFileSystem,
551
      ProcessManager: () => FakeProcessManager.any(),
552
    });
553

554 555 556 557 558 559 560 561 562 563 564 565 566
    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(),
    });

567 568
    testUsingContext('exits when multiple devices connected', () async {
      Device aDeviceWithId(String id) {
569
        final MockAndroidDevice device = MockAndroidDevice();
570 571 572 573 574 575 576
        when(device.name).thenReturn('d$id');
        when(device.id).thenReturn(id);
        when(device.isLocalEmulator).thenAnswer((_) async => false);
        when(device.sdkNameAndVersion).thenAnswer((_) async => 'Android 46');
        return device;
      }

577
      final AttachCommand command = AttachCommand();
578 579 580 581
      testDeviceManager.addDevice(aDeviceWithId('xx1'));
      testDeviceManager.addDevice(aDeviceWithId('yy2'));
      await expectLater(
        createTestCommandRunner(command).run(<String>['attach']),
Dan Field's avatar
Dan Field committed
582
        throwsToolExit(),
583
      );
584
      expect(testLogger.statusText, containsIgnoringWhitespace('More than one device'));
585 586
      expect(testLogger.statusText, contains('xx1'));
      expect(testLogger.statusText, contains('yy2'));
587 588
    }, overrides: <Type, Generator>{
      FileSystem: () => testFileSystem,
589
      ProcessManager: () => FakeProcessManager.any(),
590
    });
591 592 593
  });
}

594 595
class MockHotRunner extends Mock implements HotRunner {}
class MockHotRunnerFactory extends Mock implements HotRunnerFactory {}
596 597 598
class MockIOSDevice extends Mock implements IOSDevice {}
class MockMDnsObservatoryDiscovery extends Mock implements MDnsObservatoryDiscovery {}
class MockPortForwarder extends Mock implements DevicePortForwarder {}
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643

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');
644 645 646 647 648
    return SilentStatus(
      timeout: timeout,
      timeoutConfiguration: timeoutConfiguration,
      stopwatch: Stopwatch(),
    )..start();
649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667
  }

  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;
668 669

  @override
670
  void sendEvent(String name, [Map<String, dynamic> args]) { }
671 672 673 674 675 676

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

  @override
  bool get hasTerminal => false;
677 678 679

  @override
  void clear() => _log('[stdout] ${globals.terminal.clearScreen()}\n');
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694
}

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.
  }
}
695 696 697 698 699 700 701 702 703 704 705

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

  return (
    Uri httpUri, {
    ReloadSources reloadSources,
    Restart restart,
    CompileExpression compileExpression,
706
    ReloadMethod reloadMethod,
707
    GetSkSLMethod getSkSLMethod,
708
    PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
709
    CompressionOptions compression,
710
    Device device,
711
  }) async {
712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755
    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;
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 796 797 798 799 800 801
  };
}

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

class MockProcessManager extends Mock implements ProcessManager {}
class MockProcess extends Mock implements Process {}
class MockHttpClientRequest extends Mock implements HttpClientRequest {}
class MockHttpClientResponse extends Mock implements HttpClientResponse {}
802
class MockHttpHeaders extends Mock implements HttpHeaders {}