fuchsia_device_test.dart 32.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7 8
import 'dart:async';

9 10
import 'package:file/memory.dart';
import 'package:flutter_tools/src/application_package.dart';
11
import 'package:flutter_tools/src/artifacts.dart';
12
import 'package:flutter_tools/src/base/dds.dart';
13
import 'package:flutter_tools/src/base/file_system.dart';
14
import 'package:flutter_tools/src/base/io.dart';
15 16
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
17
import 'package:flutter_tools/src/base/time.dart';
18
import 'package:flutter_tools/src/build_info.dart';
19
import 'package:flutter_tools/src/cache.dart';
20
import 'package:flutter_tools/src/device.dart';
21
import 'package:flutter_tools/src/device_port_forwarder.dart';
22
import 'package:flutter_tools/src/fuchsia/fuchsia_device.dart';
23
import 'package:flutter_tools/src/fuchsia/fuchsia_ffx.dart';
24 25
import 'package:flutter_tools/src/fuchsia/fuchsia_kernel_compiler.dart';
import 'package:flutter_tools/src/fuchsia/fuchsia_pm.dart';
26
import 'package:flutter_tools/src/fuchsia/fuchsia_sdk.dart';
27
import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
28
import 'package:flutter_tools/src/globals.dart' as globals;
29 30
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/vmservice.dart';
31
import 'package:meta/meta.dart';
32
import 'package:test/fake.dart';
33
import 'package:vm_service/vm_service.dart' as vm_service;
34

35 36
import '../../src/common.dart';
import '../../src/context.dart';
37
import '../../src/fake_vm_services.dart';
38

39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
final vm_service.Isolate fakeIsolate = 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: 'wrong name',
  number: '1',
  pauseOnExit: false,
  runnable: true,
  startTime: 0,
54
  isSystemIsolate: false,
55
  isolateFlags: <vm_service.IsolateFlag>[],
56 57
);

58 59
void main() {
  group('fuchsia device', () {
60
    MemoryFileSystem memoryFileSystem;
61
    File sshConfig;
62
    FakeProcessManager processManager;
Dan Field's avatar
Dan Field committed
63

64
    setUp(() {
65 66
      memoryFileSystem = MemoryFileSystem.test();
      sshConfig = memoryFileSystem.file('ssh_config')..writeAsStringSync('\n');
67
      processManager = FakeProcessManager.empty();
68 69
    });

70
    testWithoutContext('stores the requested id and name', () {
71 72 73
      const String deviceId = 'e80::0000:a00a:f00f:2002/3';
      const String name = 'halfbaked';
      final FuchsiaDevice device = FuchsiaDevice(deviceId, name: name);
74

75 76 77 78
      expect(device.id, deviceId);
      expect(device.name, name);
    });

79 80 81 82 83 84 85 86 87 88 89
    testWithoutContext('supports all runtime modes besides jitRelease', () {
      const String deviceId = 'e80::0000:a00a:f00f:2002/3';
      const String name = 'halfbaked';
      final FuchsiaDevice device = FuchsiaDevice(deviceId, name: name);

      expect(device.supportsRuntimeMode(BuildMode.debug), true);
      expect(device.supportsRuntimeMode(BuildMode.profile), true);
      expect(device.supportsRuntimeMode(BuildMode.release), true);
      expect(device.supportsRuntimeMode(BuildMode.jitRelease), false);
    });

90
    testWithoutContext('lists nothing when workflow cannot list devices', () async {
91
      final FakeFuchsiaWorkflow fuchsiaWorkflow = FakeFuchsiaWorkflow(canListDevices: false);
92
      final FuchsiaDevices fuchsiaDevices = FuchsiaDevices(
93
        platform: FakePlatform(),
94
        fuchsiaSdk: FakeFuchsiaSdk(devices: 'ignored'),
95 96 97
        fuchsiaWorkflow: fuchsiaWorkflow,
        logger: BufferLogger.test(),
      );
98

99 100
      expect(fuchsiaDevices.canListAnything, false);
      expect(await fuchsiaDevices.pollingGetDevices(), isEmpty);
101
    });
102

103
    testWithoutContext('can parse ffx output for single device', () async {
104
      final FakeFuchsiaWorkflow fuchsiaWorkflow = FakeFuchsiaWorkflow();
105
      final FakeFuchsiaSdk fuchsiaSdk = FakeFuchsiaSdk(devices: '2001:0db8:85a3:0000:0000:8a2e:0370:7334 paper-pulp-bush-angel');
106
      final FuchsiaDevices fuchsiaDevices = FuchsiaDevices(
107
        platform: FakePlatform(environment: <String, String>{}),
108 109 110 111
        fuchsiaSdk: fuchsiaSdk,
        fuchsiaWorkflow: fuchsiaWorkflow,
        logger: BufferLogger.test(),
      );
112

113 114 115 116 117 118
      final Device device = (await fuchsiaDevices.pollingGetDevices()).single;

      expect(device.name, 'paper-pulp-bush-angel');
      expect(device.id, '192.168.42.10');
    });

119
    testWithoutContext('can parse ffx output for multiple devices', () async {
120
      final FakeFuchsiaWorkflow fuchsiaWorkflow = FakeFuchsiaWorkflow();
121 122 123 124
      final FakeFuchsiaSdk fuchsiaSdk = FakeFuchsiaSdk(devices:
        '2001:0db8:85a3:0000:0000:8a2e:0370:7334 paper-pulp-bush-angel\n'
        '2001:0db8:85a3:0000:0000:8a2e:0370:7335 foo-bar-fiz-buzz'
      );
125
      final FuchsiaDevices fuchsiaDevices = FuchsiaDevices(
126
        platform: FakePlatform(),
127 128 129 130 131 132 133 134 135 136 137 138 139
        fuchsiaSdk: fuchsiaSdk,
        fuchsiaWorkflow: fuchsiaWorkflow,
        logger: BufferLogger.test(),
      );

      final List<Device> devices = await fuchsiaDevices.pollingGetDevices();

      expect(devices.first.name, 'paper-pulp-bush-angel');
      expect(devices.first.id, '192.168.42.10');
      expect(devices.last.name, 'foo-bar-fiz-buzz');
      expect(devices.last.id, '192.168.42.10');
    });

140
    testWithoutContext('can parse junk output from ffx', () async {
141 142
      final FakeFuchsiaWorkflow fuchsiaWorkflow = FakeFuchsiaWorkflow(canListDevices: false);
      final FakeFuchsiaSdk fuchsiaSdk = FakeFuchsiaSdk(devices: 'junk');
143
      final FuchsiaDevices fuchsiaDevices = FuchsiaDevices(
144
        platform: FakePlatform(),
145 146 147 148 149 150 151 152 153 154
        fuchsiaSdk: fuchsiaSdk,
        fuchsiaWorkflow: fuchsiaWorkflow,
        logger: BufferLogger.test(),
      );

      final List<Device> devices = await fuchsiaDevices.pollingGetDevices();

      expect(devices, isEmpty);
    });

155
    testUsingContext('disposing device disposes the portForwarder', () async {
156
      final FakePortForwarder portForwarder = FakePortForwarder();
157
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
158
      device.portForwarder = portForwarder;
159
      await device.dispose();
160 161

      expect(portForwarder.disposed, true);
162 163
    });

164
    testWithoutContext('default capabilities', () async {
165
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
166 167 168
      final FlutterProject project = FlutterProject.fromDirectoryTest(memoryFileSystem.currentDirectory);
      memoryFileSystem.directory('fuchsia').createSync(recursive: true);
      memoryFileSystem.file('pubspec.yaml').createSync();
169 170 171

      expect(device.supportsHotReload, true);
      expect(device.supportsHotRestart, false);
172
      expect(device.supportsFlutterExit, false);
173
      expect(device.isSupportedForProject(project), true);
174
    });
175 176

    test('is ephemeral', () {
177
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
178

179 180
      expect(device.ephemeral, true);
    });
181

182
    testWithoutContext('supported for project', () async {
183
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
184 185 186
      final FlutterProject project = FlutterProject.fromDirectoryTest(memoryFileSystem.currentDirectory);
      memoryFileSystem.directory('fuchsia').createSync(recursive: true);
      memoryFileSystem.file('pubspec.yaml').createSync();
187

188
      expect(device.isSupportedForProject(project), true);
189 190
    });

191
    testWithoutContext('not supported for project', () async {
192
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
193 194
      final FlutterProject project = FlutterProject.fromDirectoryTest(memoryFileSystem.currentDirectory);
      memoryFileSystem.file('pubspec.yaml').createSync();
195

196
      expect(device.isSupportedForProject(project), false);
197
    });
198

199
    testUsingContext('targetPlatform does not throw when sshConfig is missing', () async {
200
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
201

202 203
      expect(await device.targetPlatform, TargetPlatform.fuchsia_arm64);
    }, overrides: <Type, Generator>{
204
      FuchsiaArtifacts: () => FuchsiaArtifacts(),
205 206
      FuchsiaSdk: () => FakeFuchsiaSdk(),
      ProcessManager: () => processManager,
207 208
    });

209
    testUsingContext('targetPlatform arm64 works', () async {
210 211 212 213 214
      processManager.addCommand(const FakeCommand(
        command: <String>['ssh', '-F', '/ssh_config', '123', 'uname -m'],
        stdout: 'aarch64',
      ));

215
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
216 217 218
      expect(await device.targetPlatform, TargetPlatform.fuchsia_arm64);
    }, overrides: <Type, Generator>{
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
219 220
      FuchsiaSdk: () => FakeFuchsiaSdk(),
      ProcessManager: () => processManager,
221 222 223
    });

    testUsingContext('targetPlatform x64 works', () async {
224 225 226 227 228
      processManager.addCommand(const FakeCommand(
        command: <String>['ssh', '-F', '/ssh_config', '123', 'uname -m'],
        stdout: 'x86_64',
      ));

229
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
230 231 232
      expect(await device.targetPlatform, TargetPlatform.fuchsia_x64);
    }, overrides: <Type, Generator>{
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
233 234
      FuchsiaSdk: () => FakeFuchsiaSdk(),
      ProcessManager: () => processManager,
235 236 237
    });

    testUsingContext('hostAddress parsing works', () async {
238 239 240 241 242
      processManager.addCommand(const FakeCommand(
        command: <String>['ssh', '-F', '/ssh_config', 'id', r'echo $SSH_CONNECTION'],
        stdout: 'fe80::8c6c:2fff:fe3d:c5e1%ethp0003 50666 fe80::5054:ff:fe63:5e7a%ethp0003 22',
      ));

243
      final FuchsiaDevice device = FuchsiaDevice('id', name: 'device');
244 245 246
      expect(await device.hostAddress, 'fe80::8c6c:2fff:fe3d:c5e1%25ethp0003');
    }, overrides: <Type, Generator>{
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
247 248
      FuchsiaSdk: () => FakeFuchsiaSdk(),
      ProcessManager: () => processManager,
249 250 251
    });

    testUsingContext('hostAddress parsing throws tool error on failure', () async {
252 253 254 255 256
      processManager.addCommand(const FakeCommand(
        command: <String>['ssh', '-F', '/ssh_config', 'id', r'echo $SSH_CONNECTION'],
        exitCode: 1,
      ));

257
      final FuchsiaDevice device = FuchsiaDevice('id', name: 'device');
258
      await expectLater(() => device.hostAddress, throwsToolExit());
259 260
    }, overrides: <Type, Generator>{
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
261 262
      FuchsiaSdk: () => FakeFuchsiaSdk(),
      ProcessManager: () => processManager,
263 264 265
    });

    testUsingContext('hostAddress parsing throws tool error on empty response', () async {
266 267 268 269
      processManager.addCommand(const FakeCommand(
        command: <String>['ssh', '-F', '/ssh_config', 'id', r'echo $SSH_CONNECTION'],
      ));

270
      final FuchsiaDevice device = FuchsiaDevice('id', name: 'device');
271
      expect(() async => device.hostAddress, throwsToolExit());
272 273
    }, overrides: <Type, Generator>{
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
274 275
      FuchsiaSdk: () => FakeFuchsiaSdk(),
      ProcessManager: () => processManager,
276
    });
277 278 279
  });

  group('displays friendly error when', () {
280
    File artifactFile;
281
    FakeProcessManager processManager;
282 283

    setUp(() {
284
      processManager = FakeProcessManager.empty();
285
      artifactFile = MemoryFileSystem.test().file('artifact');
286
    });
287 288

    testUsingContext('No vmservices found', () async {
289 290 291
      processManager.addCommand(const FakeCommand(
        command: <String>['ssh', '-F', '/artifact', 'id', 'find /hub -name vmservice-port'],
      ));
292
      final FuchsiaDevice device = FuchsiaDevice('id', name: 'device');
293 294

      await expectLater(device.servicePorts, throwsToolExit(message: 'No Dart Observatories found. Are you running a debug build?'));
295
    }, overrides: <Type, Generator>{
296
      ProcessManager: () => processManager,
297
      FuchsiaArtifacts: () => FuchsiaArtifacts(
298 299 300 301
        sshConfig: artifactFile,
        ffx: artifactFile,
      ),
      FuchsiaSdk: () => FakeFuchsiaSdk(),
302
    });
303 304 305

    group('device logs', () {
      const String exampleUtcLogs = '''
306
[2018-11-09 01:27:45][3][297950920][log] INFO: example_app.cmx(flutter): Error doing thing
307 308
[2018-11-09 01:27:58][46257][46269][foo] INFO: Using a thing
[2018-11-09 01:29:58][46257][46269][foo] INFO: Blah blah blah
309
[2018-11-09 01:29:58][46257][46269][foo] INFO: other_app.cmx(flutter): Do thing
310
[2018-11-09 01:30:02][41175][41187][bar] INFO: Invoking a bar
311
[2018-11-09 01:30:12][52580][52983][log] INFO: example_app.cmx(flutter): Did thing this time
312

313
  ''';
314
      FakeProcessManager processManager;
315
      File ffx;
316
      File sshConfig;
317

318
      setUp(() {
319
       processManager = FakeProcessManager.empty();
320
        final FileSystem memoryFileSystem = MemoryFileSystem.test();
321
        ffx = memoryFileSystem.file('ffx')..writeAsStringSync('\n');
322
        sshConfig = memoryFileSystem.file('ssh_config')..writeAsStringSync('\n');
323 324 325
      });

      testUsingContext('can be parsed for an app', () async {
326 327 328 329 330 331
        final Completer<void> lock = Completer<void>();
        processManager.addCommand(FakeCommand(
          command: const <String>['ssh', '-F', '/ssh_config', 'id', 'log_listener --clock Local'],
          stdout: exampleUtcLogs,
          completer: lock,
        ));
332
        final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
333
        final DeviceLogReader reader = device.getLogReader(app: FuchsiaModulePackage(name: 'example_app'));
334
        final List<String> logLines = <String>[];
335 336 337 338 339 340
        reader.logLines.listen((String line) {
          logLines.add(line);
          if (logLines.length == 2) {
            lock.complete();
          }
        });
341 342
        expect(logLines, isEmpty);

343
        await lock.future;
344 345 346 347 348
        expect(logLines, <String>[
          '[2018-11-09 01:27:45.000] Flutter: Error doing thing',
          '[2018-11-09 01:30:12.000] Flutter: Did thing this time',
        ]);
      }, overrides: <Type, Generator>{
349
        ProcessManager: () => processManager,
350
        SystemClock: () => SystemClock.fixed(DateTime(2018, 11, 9, 1, 25, 45)),
351
        FuchsiaArtifacts: () =>
352
            FuchsiaArtifacts(sshConfig: sshConfig, ffx: ffx),
353 354 355
      });

      testUsingContext('cuts off prior logs', () async {
356 357 358 359 360 361
        final Completer<void> lock = Completer<void>();
        processManager.addCommand(FakeCommand(
          command: const <String>['ssh', '-F', '/ssh_config', 'id', 'log_listener --clock Local'],
          stdout: exampleUtcLogs,
          completer: lock,
        ));
362
        final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
363
        final DeviceLogReader reader = device.getLogReader(app: FuchsiaModulePackage(name: 'example_app'));
364 365 366 367 368 369 370
        final List<String> logLines = <String>[];
        reader.logLines.listen((String line) {
          logLines.add(line);
          lock.complete();
        });
        expect(logLines, isEmpty);

371
        await lock.future.timeout(const Duration(seconds: 1));
372 373 374 375 376

        expect(logLines, <String>[
          '[2018-11-09 01:30:12.000] Flutter: Did thing this time',
        ]);
      }, overrides: <Type, Generator>{
377
        ProcessManager: () => processManager,
378
        SystemClock: () => SystemClock.fixed(DateTime(2018, 11, 9, 1, 29, 45)),
379
        FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig, ffx: ffx),
380 381 382
      });

      testUsingContext('can be parsed for all apps', () async {
383 384 385 386 387 388
        final Completer<void> lock = Completer<void>();
        processManager.addCommand(FakeCommand(
          command: const <String>['ssh', '-F', '/ssh_config', 'id', 'log_listener --clock Local'],
          stdout: exampleUtcLogs,
          completer: lock,
        ));
389 390 391
        final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
        final DeviceLogReader reader = device.getLogReader();
        final List<String> logLines = <String>[];
392 393 394 395 396 397
        reader.logLines.listen((String line) {
          logLines.add(line);
          if (logLines.length == 3) {
            lock.complete();
          }
        });
398 399
        expect(logLines, isEmpty);

400
        await lock.future.timeout(const Duration(seconds: 1));
401 402 403 404 405 406 407

        expect(logLines, <String>[
          '[2018-11-09 01:27:45.000] Flutter: Error doing thing',
          '[2018-11-09 01:29:58.000] Flutter: Do thing',
          '[2018-11-09 01:30:12.000] Flutter: Did thing this time',
        ]);
      }, overrides: <Type, Generator>{
408
        ProcessManager: () => processManager,
409
        SystemClock: () => SystemClock.fixed(DateTime(2018, 11, 9, 1, 25, 45)),
410
        FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig, ffx: ffx),
411 412
      });
    });
413
  });
414

Dan Field's avatar
Dan Field committed
415
  group('screenshot', () {
416 417 418 419 420 421
    FakeProcessManager processManager;

    setUp(() {
      processManager = FakeProcessManager.empty();
    });

422
    testUsingContext('is supported on posix platforms', () {
423 424
      final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
      expect(device.supportsScreenshot, true);
425
    }, overrides: <Type, Generator>{
426
      Platform: () => FakePlatform(),
427
    });
428 429 430

    testUsingContext('is not supported on Windows', () {
      final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
431

432 433 434 435 436 437 438
      expect(device.supportsScreenshot, false);
    }, overrides: <Type, Generator>{
      Platform: () => FakePlatform(
        operatingSystem: 'windows',
      ),
    });

439
    test("takeScreenshot throws if file isn't .ppm", () async {
440 441 442
      final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
      await expectLater(
        () => device.takeScreenshot(globals.fs.file('file.invalid')),
443 444 445 446 447
        throwsA(isA<Exception>().having(
          (Exception exception) => exception.toString(),
          'message',
          contains('file.invalid must be a .ppm file')
        )),
448
      );
449
    });
450 451

    testUsingContext('takeScreenshot throws if screencap failed', () async {
452 453
      processManager.addCommand(const FakeCommand(
        command: <String>[
454 455 456 457 458 459
          'ssh',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0',
          'screencap > /tmp/screenshot.ppm',
        ],
460 461 462 463
        exitCode: 1,
        stderr: '<error-message>'
      ));
      final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester');
464 465 466

      await expectLater(
        () => device.takeScreenshot(globals.fs.file('file.ppm')),
467 468 469 470 471
        throwsA(isA<Exception>().having(
          (Exception exception) => exception.toString(),
          'message',
          contains('Could not take a screenshot on device tester:\n<error-message>')
        )),
472 473
      );
    }, overrides: <Type, Generator>{
474
      ProcessManager: () => processManager,
475
      FileSystem: () => MemoryFileSystem.test(),
476 477 478 479 480
      Platform: () => FakePlatform(
        environment: <String, String>{
          'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
        },
      ),
481
    });
482 483 484

    testUsingContext('takeScreenshot throws if scp failed', () async {
      final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester');
485 486
      processManager.addCommand(const FakeCommand(
        command: <String>[
487 488 489 490 491 492
          'ssh',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0',
          'screencap > /tmp/screenshot.ppm',
        ],
493 494 495
      ));
      processManager.addCommand(const FakeCommand(
        command: <String>[
496 497 498 499 500 501
          'scp',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0:/tmp/screenshot.ppm',
          'file.ppm',
        ],
502 503 504 505 506
        exitCode: 1,
        stderr: '<error-message>',
      ));
      processManager.addCommand(const FakeCommand(
        command: <String>[
507 508 509 510 511 512
          'ssh',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0',
          'rm /tmp/screenshot.ppm',
        ],
513
      ));
514 515 516

      await expectLater(
        () => device.takeScreenshot(globals.fs.file('file.ppm')),
517 518 519 520 521
        throwsA(isA<Exception>().having(
          (Exception exception) => exception.toString(),
          'message',
          contains('Failed to copy screenshot from device:\n<error-message>')
        )),
522 523
      );
    }, overrides: <Type, Generator>{
524
      ProcessManager: () => processManager,
525
      FileSystem: () => MemoryFileSystem.test(),
526 527 528 529 530
      Platform: () => FakePlatform(
        environment: <String, String>{
          'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
        },
      ),
531
    });
532

533
    testUsingContext("takeScreenshot prints error if can't delete file from device", () async {
534
      final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester');
535 536
      processManager.addCommand(const FakeCommand(
        command: <String>[
537 538 539 540 541 542
          'ssh',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0',
          'screencap > /tmp/screenshot.ppm',
        ],
543 544 545
      ));
      processManager.addCommand(const FakeCommand(
        command: <String>[
546 547 548 549 550 551
          'scp',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0:/tmp/screenshot.ppm',
          'file.ppm',
        ],
552 553 554
      ));
      processManager.addCommand(const FakeCommand(
        command: <String>[
555 556 557 558 559 560
          'ssh',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0',
          'rm /tmp/screenshot.ppm',
        ],
561 562 563 564 565
        exitCode: 1,
        stderr: '<error-message>',
      ));

      await device.takeScreenshot(globals.fs.file('file.ppm'));
566 567 568 569 570
      expect(
        testLogger.errorText,
        contains('Failed to delete screenshot.ppm from the device:\n<error-message>'),
      );
    }, overrides: <Type, Generator>{
571
      ProcessManager: () => processManager,
572
      FileSystem: () => MemoryFileSystem.test(),
573 574 575 576 577 578 579 580 581
      Platform: () => FakePlatform(
        environment: <String, String>{
          'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
        },
      ),
    }, testOn: 'posix');

    testUsingContext('takeScreenshot returns', () async {
      final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester');
582 583
      processManager.addCommand(const FakeCommand(
        command: <String>[
584 585 586 587 588 589
          'ssh',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0',
          'screencap > /tmp/screenshot.ppm',
        ],
590 591 592
      ));
      processManager.addCommand(const FakeCommand(
        command: <String>[
593 594 595 596 597 598
          'scp',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0:/tmp/screenshot.ppm',
          'file.ppm',
        ],
599 600 601
      ));
      processManager.addCommand(const FakeCommand(
        command: <String>[
602 603 604 605 606 607
          'ssh',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0',
          'rm /tmp/screenshot.ppm',
        ],
608
      ));
609

610
      expect(() => device.takeScreenshot(globals.fs.file('file.ppm')), returnsNormally);
611
    }, overrides: <Type, Generator>{
612
      ProcessManager: () => processManager,
613
      FileSystem: () => MemoryFileSystem.test(),
614 615 616 617 618
      Platform: () => FakePlatform(
        environment: <String, String>{
          'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
        },
      ),
619
    });
620 621
  });

622
  group('portForwarder', () {
623
    FakeProcessManager processManager;
624
    File sshConfig;
625 626

    setUp(() {
627
      processManager = FakeProcessManager.empty();
628
      sshConfig = MemoryFileSystem.test().file('irrelevant')..writeAsStringSync('\n');
629 630 631 632
    });

    testUsingContext('`unforward` prints stdout and stderr if ssh command failed', () async {
      final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
633 634 635 636 637 638
      processManager.addCommand(const FakeCommand(
        command: <String>['ssh', '-F', '/irrelevant', '-O', 'cancel', '-vvv', '-L', '0:127.0.0.1:1', 'id'],
        exitCode: 1,
        stdout: '<stdout>',
        stderr: '<stderr>',
      ));
639 640 641 642 643 644

      await expectLater(
        () => device.portForwarder.unforward(ForwardedPort(/*hostPort=*/ 0, /*devicePort=*/ 1)),
        throwsToolExit(message: 'Unforward command failed:\nstdout: <stdout>\nstderr: <stderr>'),
      );
    }, overrides: <Type, Generator>{
645
      ProcessManager: () => processManager,
646 647 648 649
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
    });
  });

Dan Field's avatar
Dan Field committed
650

651 652
  group('FuchsiaIsolateDiscoveryProtocol', () {
    Future<Uri> findUri(List<FlutterView> views, String expectedIsolateName) async {
653 654 655 656 657 658 659
      final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
        requests: <VmServiceExpectation>[
          FakeVmServiceRequest(
            method: kListViewsMethod,
            jsonResponse: <String, Object>{
              'views': <Object>[
                for (FlutterView view in views)
660
                  view.toJson(),
661 662 663 664
              ],
            },
          ),
        ],
665
        httpAddress: Uri.parse('example'),
666
      );
667
      final MockFuchsiaDevice fuchsiaDevice = MockFuchsiaDevice('123', const NoOpDevicePortForwarder(), false);
668
      final FuchsiaIsolateDiscoveryProtocol discoveryProtocol =
669
        FuchsiaIsolateDiscoveryProtocol(
670 671
        fuchsiaDevice,
        expectedIsolateName,
672
        (Uri uri) async => fakeVmServiceHost.vmService,
673
        (Device device, Uri uri, bool enableServiceAuthCodes) => null,
674
        true, // only poll once.
675
      );
676
      return discoveryProtocol.uri;
677
    }
678

679
    testUsingContext('can find flutter view with matching isolate name', () async {
680
      const String expectedIsolateName = 'foobar';
681 682
      final Uri uri = await findUri(<FlutterView>[
        // no ui isolate.
683
        FlutterView(id: '1', uiIsolate: fakeIsolate),
684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699
        // wrong name.
        FlutterView(
          id: '2',
          uiIsolate: vm_service.Isolate.parse(<String, dynamic>{
            ...fakeIsolate.toJson(),
            'name': 'Wrong name',
          }),
        ),
        // matching name.
        FlutterView(
          id: '3',
          uiIsolate: vm_service.Isolate.parse(<String, dynamic>{
             ...fakeIsolate.toJson(),
            'name': expectedIsolateName,
          }),
        ),
700
      ], expectedIsolateName);
701

702
      expect(uri.toString(), 'http://${InternetAddress.loopbackIPv4.address}:0/');
703 704
    });

705
    testUsingContext('can handle flutter view without matching isolate name', () async {
706
      const String expectedIsolateName = 'foobar';
707 708
      final Future<Uri> uri = findUri(<FlutterView>[
        // no ui isolate.
709
        FlutterView(id: '1', uiIsolate: fakeIsolate),
710 711 712 713 714
        // wrong name.
        FlutterView(id: '2', uiIsolate: vm_service.Isolate.parse(<String, Object>{
           ...fakeIsolate.toJson(),
          'name': 'wrong name',
        })),
715
      ], expectedIsolateName);
716

717 718 719 720 721
      expect(uri, throwsException);
    });

    testUsingContext('can handle non flutter view', () async {
      const String expectedIsolateName = 'foobar';
722
      final Future<Uri> uri = findUri(<FlutterView>[
723
        FlutterView(id: '1', uiIsolate: fakeIsolate), // no ui isolate.
724
      ], expectedIsolateName);
725

726 727 728
      expect(uri, throwsException);
    });
  });
729

730
  testUsingContext('Correct flutter runner', () async {
731 732 733
    final Cache cache = Cache.test(
      processManager: FakeProcessManager.any(),
    );
734 735 736 737
    final FileSystem fileSystem = MemoryFileSystem.test();
    final CachedArtifacts artifacts = CachedArtifacts(
      cache: cache,
      fileSystem: fileSystem,
738
      platform: FakePlatform(),
739
      operatingSystemUtils: globals.os,
740 741
    );
    expect(artifacts.getArtifactPath(
742 743 744 745 746 747
        Artifact.fuchsiaFlutterRunner,
        platform: TargetPlatform.fuchsia_x64,
        mode: BuildMode.debug,
      ),
      contains('flutter_jit_runner'),
    );
748
    expect(artifacts.getArtifactPath(
749 750 751 752 753 754
        Artifact.fuchsiaFlutterRunner,
        platform: TargetPlatform.fuchsia_x64,
        mode: BuildMode.profile,
      ),
      contains('flutter_aot_runner'),
    );
755
    expect(artifacts.getArtifactPath(
756 757 758 759 760 761
        Artifact.fuchsiaFlutterRunner,
        platform: TargetPlatform.fuchsia_x64,
        mode: BuildMode.release,
      ),
      contains('flutter_aot_product_runner'),
    );
762
    expect(artifacts.getArtifactPath(
763 764 765 766 767 768 769 770
        Artifact.fuchsiaFlutterRunner,
        platform: TargetPlatform.fuchsia_x64,
        mode: BuildMode.jitRelease,
      ),
      contains('flutter_jit_product_runner'),
    );
  });

771
  group('sdkNameAndVersion: ', () {
772
    File sshConfig;
773
    FakeProcessManager processManager;
774 775

    setUp(() {
776
      sshConfig = MemoryFileSystem.test().file('ssh_config')..writeAsStringSync('\n');
777
      processManager = FakeProcessManager.empty();
778 779
    });

780
    testUsingContext('does not throw on non-existent ssh config', () async {
781
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
782

783 784
      expect(await device.sdkNameAndVersion, equals('Fuchsia'));
    }, overrides: <Type, Generator>{
785
      ProcessManager: () => processManager,
786
      FuchsiaArtifacts: () => FuchsiaArtifacts(),
787
      FuchsiaSdk: () => FakeFuchsiaSdk(),
788 789
    });

790
    testUsingContext('returns what we get from the device on success', () async {
791 792 793 794
      processManager.addCommand(const FakeCommand(
        command: <String>['ssh', '-F', '/ssh_config', '123', 'cat /pkgfs/packages/build-info/0/data/version'],
        stdout: 'version'
      ));
795
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
796

797 798
      expect(await device.sdkNameAndVersion, equals('Fuchsia version'));
    }, overrides: <Type, Generator>{
799
      ProcessManager: () => processManager,
800
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
801
      FuchsiaSdk: () => FakeFuchsiaSdk(),
802 803 804
    });

    testUsingContext('returns "Fuchsia" when device command fails', () async {
805 806 807 808
      processManager.addCommand(const FakeCommand(
        command: <String>['ssh', '-F', '/ssh_config', '123', 'cat /pkgfs/packages/build-info/0/data/version'],
        exitCode: 1,
      ));
809
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
810

811 812
      expect(await device.sdkNameAndVersion, equals('Fuchsia'));
    }, overrides: <Type, Generator>{
813
      ProcessManager: () => processManager,
814
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
815
      FuchsiaSdk: () => FakeFuchsiaSdk(),
816 817 818
    });

    testUsingContext('returns "Fuchsia" when device gives an empty result', () async {
819 820 821
      processManager.addCommand(const FakeCommand(
        command: <String>['ssh', '-F', '/ssh_config', '123', 'cat /pkgfs/packages/build-info/0/data/version'],
      ));
822
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
823

824 825
      expect(await device.sdkNameAndVersion, equals('Fuchsia'));
    }, overrides: <Type, Generator>{
826
      ProcessManager: () => processManager,
827
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
828
      FuchsiaSdk: () => FakeFuchsiaSdk(),
829 830
    });
  });
831 832 833 834 835 836 837
}

class FuchsiaModulePackage extends ApplicationPackage {
  FuchsiaModulePackage({@required this.name}) : super(id: name);

  @override
  final String name;
838
}
839

840 841 842
// Unfortunately Device, despite not being immutable, has an `operator ==`.
// Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types
843
class MockFuchsiaDevice extends Fake implements FuchsiaDevice {
844 845 846
  MockFuchsiaDevice(this.id, this.portForwarder, this._ipv6);

  final bool _ipv6;
847 848

  @override
849
  bool get ipv6 => _ipv6;
850

851 852
  @override
  final String id;
853

854 855 856 857
  @override
  final DevicePortForwarder portForwarder;

  @override
858 859 860
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.fuchsia_arm64;

  @override
861
  String get name => 'fuchsia';
862 863

  @override
864
  Future<List<int>> servicePorts() async => <int>[1];
865 866

  @override
867
  DartDevelopmentService get dds => FakeDartDevelopmentService();
868 869
}

870 871
class FakePortForwarder extends Fake implements DevicePortForwarder {
  bool disposed = false;
872 873

  @override
874 875
  Future<void> dispose() async {
    disposed = true;
876
  }
877 878
}

879
class FakeFuchsiaFfx implements FuchsiaFfx {
880
  @override
881 882
  Future<List<String>> list({Duration timeout}) async {
    return <String>['192.168.42.172 scare-cable-skip-ffx'];
883 884 885
  }

  @override
886 887
  Future<String> resolve(String deviceName) async {
    return '192.168.42.10';
888 889 890
  }
}

891 892 893 894 895 896 897 898 899
class FakeFuchsiaSdk extends Fake implements FuchsiaSdk {
  FakeFuchsiaSdk({
    FuchsiaPM pm,
    FuchsiaKernelCompiler compiler,
    FuchsiaFfx ffx,
    String devices,
  }) : fuchsiaPM = pm,
       fuchsiaKernelCompiler = compiler,
       fuchsiaFfx = ffx ?? FakeFuchsiaFfx(),
900
       _devices = devices;
901 902

  @override
903
  final FuchsiaPM fuchsiaPM;
904 905

  @override
906
  final FuchsiaKernelCompiler fuchsiaKernelCompiler;
907 908

  @override
909
  final FuchsiaFfx fuchsiaFfx;
910

911
  final String _devices;
912

913
  @override
914
  Future<String> listDevices({Duration timeout}) async {
915
    return _devices;
916 917 918
  }
}

919
class FakeDartDevelopmentService extends Fake implements DartDevelopmentService {
920
  @override
921 922 923 924 925 926
  Future<void> startDartDevelopmentService(
    Uri observatoryUri, {
    @required Logger logger,
    int hostPort,
    bool ipv6,
    bool disableServiceAuthCodes,
927
    bool cacheStartupProfile = false,
928
  }) async {}
929 930

  @override
931
  Uri get uri => Uri.parse('example');
932 933
}

934 935 936 937 938 939 940
class FakeFuchsiaWorkflow implements FuchsiaWorkflow {
  FakeFuchsiaWorkflow({
    this.appliesToHostPlatform = true,
    this.canLaunchDevices = true,
    this.canListDevices = true,
    this.canListEmulators = true,
  });
941 942

  @override
943
  bool appliesToHostPlatform;
944

945
  @override
946
  bool canLaunchDevices;
947 948

  @override
949
  bool canListDevices;
950 951

  @override
952
  bool canListEmulators;
953
}