fuchsia_device_test.dart 34.2 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
import 'dart:async';

7 8
import 'package:file/memory.dart';
import 'package:flutter_tools/src/application_package.dart';
9
import 'package:flutter_tools/src/artifacts.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/platform.dart';
15
import 'package:flutter_tools/src/base/time.dart';
16
import 'package:flutter_tools/src/build_info.dart';
17
import 'package:flutter_tools/src/cache.dart';
18
import 'package:flutter_tools/src/device.dart';
19
import 'package:flutter_tools/src/device_port_forwarder.dart';
20
import 'package:flutter_tools/src/fuchsia/fuchsia_device.dart';
21
import 'package:flutter_tools/src/fuchsia/fuchsia_ffx.dart';
22 23
import 'package:flutter_tools/src/fuchsia/fuchsia_kernel_compiler.dart';
import 'package:flutter_tools/src/fuchsia/fuchsia_pm.dart';
24
import 'package:flutter_tools/src/fuchsia/fuchsia_sdk.dart';
25
import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
26
import 'package:flutter_tools/src/globals.dart' as globals;
27 28
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/vmservice.dart';
29
import 'package:test/fake.dart';
30
import 'package:vm_service/vm_service.dart' as vm_service;
31

32 33
import '../../src/common.dart';
import '../../src/context.dart';
34
import '../../src/fake_vm_services.dart';
35

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

54 55
void main() {
  group('fuchsia device', () {
56 57 58
    late MemoryFileSystem memoryFileSystem;
    late File sshConfig;
    late FakeProcessManager processManager;
Dan Field's avatar
Dan Field committed
59

60
    setUp(() {
61 62
      memoryFileSystem = MemoryFileSystem.test();
      sshConfig = memoryFileSystem.file('ssh_config')..writeAsStringSync('\n');
63
      processManager = FakeProcessManager.empty();
64 65
    });

66
    testWithoutContext('stores the requested id and name', () {
67 68 69
      const String deviceId = 'e80::0000:a00a:f00f:2002/3';
      const String name = 'halfbaked';
      final FuchsiaDevice device = FuchsiaDevice(deviceId, name: name);
70

71 72 73 74
      expect(device.id, deviceId);
      expect(device.name, name);
    });

75 76 77 78 79 80 81 82 83 84 85
    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);
    });

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

97 98
      expect(fuchsiaDevices.canListAnything, false);
      expect(await fuchsiaDevices.pollingGetDevices(), isEmpty);
99
    });
100

101
    testWithoutContext('can parse ffx output for single device', () async {
102
      final FakeFuchsiaWorkflow fuchsiaWorkflow = FakeFuchsiaWorkflow();
103 104 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);
143
      final FakeFuchsiaSdk fuchsiaSdk = FakeFuchsiaSdk(devices: 'junk');
144
      final FuchsiaDevices fuchsiaDevices = FuchsiaDevices(
145
        platform: FakePlatform(),
146 147 148 149 150 151 152 153 154 155
        fuchsiaSdk: fuchsiaSdk,
        fuchsiaWorkflow: fuchsiaWorkflow,
        logger: BufferLogger.test(),
      );

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

      expect(devices, isEmpty);
    });

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

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

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

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

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

181 182
      expect(device.ephemeral, true);
    });
183

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

191
      expect(device.isSupportedForProject(project), true);
192 193
    });

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

200
      expect(device.isSupportedForProject(project), false);
201
    });
202

203 204
    testUsingContext('targetPlatform does not throw when sshConfig is missing',
        () async {
205
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
206

207 208
      expect(await device.targetPlatform, TargetPlatform.fuchsia_arm64);
    }, overrides: <Type, Generator>{
209
      FuchsiaArtifacts: () => FuchsiaArtifacts(),
210 211
      FuchsiaSdk: () => FakeFuchsiaSdk(),
      ProcessManager: () => processManager,
212 213
    });

214
    testUsingContext('targetPlatform arm64 works', () async {
215 216 217 218 219
      processManager.addCommand(const FakeCommand(
        command: <String>['ssh', '-F', '/ssh_config', '123', 'uname -m'],
        stdout: 'aarch64',
      ));

220
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
221 222 223
      expect(await device.targetPlatform, TargetPlatform.fuchsia_arm64);
    }, overrides: <Type, Generator>{
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
224 225
      FuchsiaSdk: () => FakeFuchsiaSdk(),
      ProcessManager: () => processManager,
226 227 228
    });

    testUsingContext('targetPlatform x64 works', () async {
229 230 231 232 233
      processManager.addCommand(const FakeCommand(
        command: <String>['ssh', '-F', '/ssh_config', '123', 'uname -m'],
        stdout: 'x86_64',
      ));

234
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
235 236 237
      expect(await device.targetPlatform, TargetPlatform.fuchsia_x64);
    }, overrides: <Type, Generator>{
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
238 239
      FuchsiaSdk: () => FakeFuchsiaSdk(),
      ProcessManager: () => processManager,
240 241 242
    });

    testUsingContext('hostAddress parsing works', () async {
243
      processManager.addCommand(const FakeCommand(
244 245 246 247 248 249 250 251 252
        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',
253 254
      ));

255
      final FuchsiaDevice device = FuchsiaDevice('id', name: 'device');
256 257 258
      expect(await device.hostAddress, 'fe80::8c6c:2fff:fe3d:c5e1%25ethp0003');
    }, overrides: <Type, Generator>{
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
259 260
      FuchsiaSdk: () => FakeFuchsiaSdk(),
      ProcessManager: () => processManager,
261 262
    });

263 264
    testUsingContext('hostAddress parsing throws tool error on failure',
        () async {
265
      processManager.addCommand(const FakeCommand(
266 267 268 269 270 271 272
        command: <String>[
          'ssh',
          '-F',
          '/ssh_config',
          'id',
          r'echo $SSH_CONNECTION'
        ],
273 274 275
        exitCode: 1,
      ));

276
      final FuchsiaDevice device = FuchsiaDevice('id', name: 'device');
277
      await expectLater(() => device.hostAddress, throwsToolExit());
278 279
    }, overrides: <Type, Generator>{
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
280 281
      FuchsiaSdk: () => FakeFuchsiaSdk(),
      ProcessManager: () => processManager,
282 283
    });

284 285
    testUsingContext('hostAddress parsing throws tool error on empty response',
        () async {
286
      processManager.addCommand(const FakeCommand(
287 288 289 290 291 292 293
        command: <String>[
          'ssh',
          '-F',
          '/ssh_config',
          'id',
          r'echo $SSH_CONNECTION'
        ],
294 295
      ));

296
      final FuchsiaDevice device = FuchsiaDevice('id', name: 'device');
297
      expect(() async => device.hostAddress, throwsToolExit());
298 299
    }, overrides: <Type, Generator>{
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
300 301
      FuchsiaSdk: () => FakeFuchsiaSdk(),
      ProcessManager: () => processManager,
302
    });
303 304 305
  });

  group('displays friendly error when', () {
306 307
    late File artifactFile;
    late FakeProcessManager processManager;
308 309

    setUp(() {
310
      processManager = FakeProcessManager.empty();
311
      artifactFile = MemoryFileSystem.test().file('artifact');
312
    });
313 314

    testUsingContext('No vmservices found', () async {
315
      processManager.addCommand(const FakeCommand(
316 317 318 319 320 321 322
        command: <String>[
          'ssh',
          '-F',
          '/artifact',
          'id',
          'find /hub -name vmservice-port'
        ],
323
      ));
324
      final FuchsiaDevice device = FuchsiaDevice('id', name: 'device');
325

326 327 328 329 330
      await expectLater(
          device.servicePorts,
          throwsToolExit(
              message:
                  'No Dart Observatories found. Are you running a debug build?'));
331
    }, overrides: <Type, Generator>{
332
      ProcessManager: () => processManager,
333
      FuchsiaArtifacts: () => FuchsiaArtifacts(
334 335 336
            sshConfig: artifactFile,
            ffx: artifactFile,
          ),
337
      FuchsiaSdk: () => FakeFuchsiaSdk(),
338
    });
339 340 341

    group('device logs', () {
      const String exampleUtcLogs = '''
342
[2018-11-09 01:27:45][3][297950920][log] INFO: example_app.cm(flutter): Error doing thing
343 344
[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
345
[2018-11-09 01:29:58][46257][46269][foo] INFO: other_app.cm(flutter): Do thing
346
[2018-11-09 01:30:02][41175][41187][bar] INFO: Invoking a bar
347
[2018-11-09 01:30:12][52580][52983][log] INFO: example_app.cm(flutter): Did thing this time
348

349
  ''';
350 351 352
      late FakeProcessManager processManager;
      late File ffx;
      late File sshConfig;
353

354
      setUp(() {
355
        processManager = FakeProcessManager.empty();
356
        final FileSystem memoryFileSystem = MemoryFileSystem.test();
357
        ffx = memoryFileSystem.file('ffx')..writeAsStringSync('\n');
358 359
        sshConfig = memoryFileSystem.file('ssh_config')
          ..writeAsStringSync('\n');
360 361 362
      });

      testUsingContext('can be parsed for an app', () async {
363 364
        final Completer<void> lock = Completer<void>();
        processManager.addCommand(FakeCommand(
365 366 367 368 369 370 371
          command: const <String>[
            'ssh',
            '-F',
            '/ssh_config',
            'id',
            'log_listener --clock Local'
          ],
372 373 374
          stdout: exampleUtcLogs,
          completer: lock,
        ));
375
        final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
376 377
        final DeviceLogReader reader =
            device.getLogReader(app: FuchsiaModulePackage(name: 'example_app'));
378
        final List<String> logLines = <String>[];
379 380 381 382 383 384
        reader.logLines.listen((String line) {
          logLines.add(line);
          if (logLines.length == 2) {
            lock.complete();
          }
        });
385 386
        expect(logLines, isEmpty);

387
        await lock.future;
388 389 390 391 392
        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>{
393
        ProcessManager: () => processManager,
394
        SystemClock: () => SystemClock.fixed(DateTime(2018, 11, 9, 1, 25, 45)),
395
        FuchsiaArtifacts: () =>
396
            FuchsiaArtifacts(sshConfig: sshConfig, ffx: ffx),
397 398 399
      });

      testUsingContext('cuts off prior logs', () async {
400 401
        final Completer<void> lock = Completer<void>();
        processManager.addCommand(FakeCommand(
402 403 404 405 406 407 408
          command: const <String>[
            'ssh',
            '-F',
            '/ssh_config',
            'id',
            'log_listener --clock Local'
          ],
409 410 411
          stdout: exampleUtcLogs,
          completer: lock,
        ));
412
        final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
413 414
        final DeviceLogReader reader =
            device.getLogReader(app: FuchsiaModulePackage(name: 'example_app'));
415 416 417 418 419 420 421
        final List<String> logLines = <String>[];
        reader.logLines.listen((String line) {
          logLines.add(line);
          lock.complete();
        });
        expect(logLines, isEmpty);

422
        await lock.future.timeout(const Duration(seconds: 1));
423 424 425 426 427

        expect(logLines, <String>[
          '[2018-11-09 01:30:12.000] Flutter: Did thing this time',
        ]);
      }, overrides: <Type, Generator>{
428
        ProcessManager: () => processManager,
429
        SystemClock: () => SystemClock.fixed(DateTime(2018, 11, 9, 1, 29, 45)),
430 431
        FuchsiaArtifacts: () =>
            FuchsiaArtifacts(sshConfig: sshConfig, ffx: ffx),
432 433 434
      });

      testUsingContext('can be parsed for all apps', () async {
435 436
        final Completer<void> lock = Completer<void>();
        processManager.addCommand(FakeCommand(
437 438 439 440 441 442 443
          command: const <String>[
            'ssh',
            '-F',
            '/ssh_config',
            'id',
            'log_listener --clock Local'
          ],
444 445 446
          stdout: exampleUtcLogs,
          completer: lock,
        ));
447 448 449
        final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
        final DeviceLogReader reader = device.getLogReader();
        final List<String> logLines = <String>[];
450 451 452 453 454 455
        reader.logLines.listen((String line) {
          logLines.add(line);
          if (logLines.length == 3) {
            lock.complete();
          }
        });
456 457
        expect(logLines, isEmpty);

458
        await lock.future.timeout(const Duration(seconds: 1));
459 460 461 462 463 464 465

        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>{
466
        ProcessManager: () => processManager,
467
        SystemClock: () => SystemClock.fixed(DateTime(2018, 11, 9, 1, 25, 45)),
468 469
        FuchsiaArtifacts: () =>
            FuchsiaArtifacts(sshConfig: sshConfig, ffx: ffx),
470 471
      });
    });
472
  });
473

Dan Field's avatar
Dan Field committed
474
  group('screenshot', () {
475
    late FakeProcessManager processManager;
476 477 478 479 480

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

481
    testUsingContext('is supported on posix platforms', () {
482 483
      final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
      expect(device.supportsScreenshot, true);
484
    }, overrides: <Type, Generator>{
485
      Platform: () => FakePlatform(),
486
    });
487 488 489

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

491 492 493
      expect(device.supportsScreenshot, false);
    }, overrides: <Type, Generator>{
      Platform: () => FakePlatform(
494 495
            operatingSystem: 'windows',
          ),
496 497
    });

498
    test("takeScreenshot throws if file isn't .ppm", () async {
499 500 501
      final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
      await expectLater(
        () => device.takeScreenshot(globals.fs.file('file.invalid')),
502
        throwsA(isA<Exception>().having(
503 504 505
            (Exception exception) => exception.toString(),
            'message',
            contains('file.invalid must be a .ppm file'))),
506
      );
507
    });
508 509

    testUsingContext('takeScreenshot throws if screencap failed', () async {
510 511 512 513 514 515 516
      processManager.addCommand(const FakeCommand(command: <String>[
        'ssh',
        '-F',
        '/fuchsia/out/default/.ssh',
        '0.0.0.0',
        'screencap > /tmp/screenshot.ppm',
      ], exitCode: 1, stderr: '<error-message>'));
517
      final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester');
518 519 520

      await expectLater(
        () => device.takeScreenshot(globals.fs.file('file.ppm')),
521
        throwsA(isA<Exception>().having(
522 523 524 525
            (Exception exception) => exception.toString(),
            'message',
            contains(
                'Could not take a screenshot on device tester:\n<error-message>'))),
526 527
      );
    }, overrides: <Type, Generator>{
528
      ProcessManager: () => processManager,
529
      FileSystem: () => MemoryFileSystem.test(),
530
      Platform: () => FakePlatform(
531 532 533 534
            environment: <String, String>{
              'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
            },
          ),
535
    });
536 537 538

    testUsingContext('takeScreenshot throws if scp failed', () async {
      final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester');
539 540
      processManager.addCommand(const FakeCommand(
        command: <String>[
541 542 543 544 545 546
          'ssh',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0',
          'screencap > /tmp/screenshot.ppm',
        ],
547 548 549
      ));
      processManager.addCommand(const FakeCommand(
        command: <String>[
550 551 552 553 554 555
          'scp',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0:/tmp/screenshot.ppm',
          'file.ppm',
        ],
556 557 558 559 560
        exitCode: 1,
        stderr: '<error-message>',
      ));
      processManager.addCommand(const FakeCommand(
        command: <String>[
561 562 563 564 565 566
          'ssh',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0',
          'rm /tmp/screenshot.ppm',
        ],
567
      ));
568 569 570

      await expectLater(
        () => device.takeScreenshot(globals.fs.file('file.ppm')),
571
        throwsA(isA<Exception>().having(
572 573 574 575
            (Exception exception) => exception.toString(),
            'message',
            contains(
                'Failed to copy screenshot from device:\n<error-message>'))),
576 577
      );
    }, overrides: <Type, Generator>{
578
      ProcessManager: () => processManager,
579
      FileSystem: () => MemoryFileSystem.test(),
580
      Platform: () => FakePlatform(
581 582 583 584
            environment: <String, String>{
              'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
            },
          ),
585
    });
586

587 588 589
    testUsingContext(
        "takeScreenshot prints error if can't delete file from device",
        () async {
590
      final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester');
591 592
      processManager.addCommand(const FakeCommand(
        command: <String>[
593 594 595 596 597 598
          'ssh',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0',
          'screencap > /tmp/screenshot.ppm',
        ],
599 600 601
      ));
      processManager.addCommand(const FakeCommand(
        command: <String>[
602 603 604 605 606 607
          'scp',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0:/tmp/screenshot.ppm',
          'file.ppm',
        ],
608 609 610
      ));
      processManager.addCommand(const FakeCommand(
        command: <String>[
611 612 613 614 615 616
          'ssh',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0',
          'rm /tmp/screenshot.ppm',
        ],
617 618 619 620 621
        exitCode: 1,
        stderr: '<error-message>',
      ));

      await device.takeScreenshot(globals.fs.file('file.ppm'));
622 623
      expect(
        testLogger.errorText,
624 625
        contains(
            'Failed to delete screenshot.ppm from the device:\n<error-message>'),
626 627
      );
    }, overrides: <Type, Generator>{
628
      ProcessManager: () => processManager,
629
      FileSystem: () => MemoryFileSystem.test(),
630
      Platform: () => FakePlatform(
631 632 633 634
            environment: <String, String>{
              'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
            },
          ),
635 636 637 638
    }, testOn: 'posix');

    testUsingContext('takeScreenshot returns', () async {
      final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester');
639 640
      processManager.addCommand(const FakeCommand(
        command: <String>[
641 642 643 644 645 646
          'ssh',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0',
          'screencap > /tmp/screenshot.ppm',
        ],
647 648 649
      ));
      processManager.addCommand(const FakeCommand(
        command: <String>[
650 651 652 653 654 655
          'scp',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0:/tmp/screenshot.ppm',
          'file.ppm',
        ],
656 657 658
      ));
      processManager.addCommand(const FakeCommand(
        command: <String>[
659 660 661 662 663 664
          'ssh',
          '-F',
          '/fuchsia/out/default/.ssh',
          '0.0.0.0',
          'rm /tmp/screenshot.ppm',
        ],
665
      ));
666

667 668
      expect(() => device.takeScreenshot(globals.fs.file('file.ppm')),
          returnsNormally);
669
    }, overrides: <Type, Generator>{
670
      ProcessManager: () => processManager,
671
      FileSystem: () => MemoryFileSystem.test(),
672
      Platform: () => FakePlatform(
673 674 675 676
            environment: <String, String>{
              'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
            },
          ),
677
    });
678 679
  });

680
  group('portForwarder', () {
681 682
    late FakeProcessManager processManager;
    late File sshConfig;
683 684

    setUp(() {
685
      processManager = FakeProcessManager.empty();
686 687
      sshConfig = MemoryFileSystem.test().file('irrelevant')
        ..writeAsStringSync('\n');
688 689
    });

690 691
    testUsingContext(
        '`unforward` prints stdout and stderr if ssh command failed', () async {
692
      final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
693
      processManager.addCommand(const FakeCommand(
694 695 696 697 698 699 700 701 702 703 704
        command: <String>[
          'ssh',
          '-F',
          '/irrelevant',
          '-O',
          'cancel',
          '-vvv',
          '-L',
          '0:127.0.0.1:1',
          'id'
        ],
705 706 707 708
        exitCode: 1,
        stdout: '<stdout>',
        stderr: '<stderr>',
      ));
709 710

      await expectLater(
711 712 713 714 715
        () => device.portForwarder
            .unforward(ForwardedPort(/*hostPort=*/ 0, /*devicePort=*/ 1)),
        throwsToolExit(
            message:
                'Unforward command failed:\nstdout: <stdout>\nstderr: <stderr>'),
716 717
      );
    }, overrides: <Type, Generator>{
718
      ProcessManager: () => processManager,
719 720 721 722
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
    });
  });

723
  group('FuchsiaIsolateDiscoveryProtocol', () {
724 725
    Future<Uri> findUri(
        List<FlutterView> views, String expectedIsolateName) async {
726 727 728 729 730 731
      final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
        requests: <VmServiceExpectation>[
          FakeVmServiceRequest(
            method: kListViewsMethod,
            jsonResponse: <String, Object>{
              'views': <Object>[
732
                for (FlutterView view in views) view.toJson(),
733 734 735 736
              ],
            },
          ),
        ],
737
        httpAddress: Uri.parse('example'),
738
      );
739 740
      final MockFuchsiaDevice fuchsiaDevice =
          MockFuchsiaDevice('123', const NoOpDevicePortForwarder(), false);
741
      final FuchsiaIsolateDiscoveryProtocol discoveryProtocol =
742
          FuchsiaIsolateDiscoveryProtocol(
743 744
        fuchsiaDevice,
        expectedIsolateName,
745
        (Uri uri) async => fakeVmServiceHost.vmService,
746
        (Device device, Uri uri, bool enableServiceAuthCodes) async {},
747
        true, // only poll once.
748
      );
749
      return discoveryProtocol.uri;
750
    }
751

752 753
    testUsingContext('can find flutter view with matching isolate name',
        () async {
754
      const String expectedIsolateName = 'foobar';
755 756
      final Uri uri = await findUri(<FlutterView>[
        // no ui isolate.
757
        FlutterView(id: '1', uiIsolate: fakeIsolate),
758 759 760 761 762 763 764 765 766 767 768 769
        // 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>{
770
            ...fakeIsolate.toJson(),
771 772 773
            'name': expectedIsolateName,
          }),
        ),
774
      ], expectedIsolateName);
775

776 777
      expect(
          uri.toString(), 'http://${InternetAddress.loopbackIPv4.address}:0/');
778 779
    });

780 781
    testUsingContext('can handle flutter view without matching isolate name',
        () async {
782
      const String expectedIsolateName = 'foobar';
783 784
      final Future<Uri> uri = findUri(<FlutterView>[
        // no ui isolate.
785
        FlutterView(id: '1', uiIsolate: fakeIsolate),
786
        // wrong name.
787 788
        FlutterView(
            id: '2',
789
            uiIsolate: vm_service.Isolate.parse(<String, Object?>{
790 791 792
              ...fakeIsolate.toJson(),
              'name': 'wrong name',
            })),
793
      ], expectedIsolateName);
794

795 796 797 798 799
      expect(uri, throwsException);
    });

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

804 805 806
      expect(uri, throwsException);
    });
  });
807

808
  testUsingContext('Correct flutter runner', () async {
809 810 811
    final Cache cache = Cache.test(
      processManager: FakeProcessManager.any(),
    );
812 813 814 815
    final FileSystem fileSystem = MemoryFileSystem.test();
    final CachedArtifacts artifacts = CachedArtifacts(
      cache: cache,
      fileSystem: fileSystem,
816
      platform: FakePlatform(),
817
      operatingSystemUtils: globals.os,
818
    );
819 820
    expect(
      artifacts.getArtifactPath(
821 822 823 824 825 826
        Artifact.fuchsiaFlutterRunner,
        platform: TargetPlatform.fuchsia_x64,
        mode: BuildMode.debug,
      ),
      contains('flutter_jit_runner'),
    );
827 828
    expect(
      artifacts.getArtifactPath(
829 830 831 832 833 834
        Artifact.fuchsiaFlutterRunner,
        platform: TargetPlatform.fuchsia_x64,
        mode: BuildMode.profile,
      ),
      contains('flutter_aot_runner'),
    );
835 836
    expect(
      artifacts.getArtifactPath(
837 838 839 840 841 842
        Artifact.fuchsiaFlutterRunner,
        platform: TargetPlatform.fuchsia_x64,
        mode: BuildMode.release,
      ),
      contains('flutter_aot_product_runner'),
    );
843 844
    expect(
      artifacts.getArtifactPath(
845 846 847 848 849 850 851 852
        Artifact.fuchsiaFlutterRunner,
        platform: TargetPlatform.fuchsia_x64,
        mode: BuildMode.jitRelease,
      ),
      contains('flutter_jit_product_runner'),
    );
  });

853
  group('sdkNameAndVersion: ', () {
854 855
    late File sshConfig;
    late FakeProcessManager processManager;
856 857

    setUp(() {
858 859
      sshConfig = MemoryFileSystem.test().file('ssh_config')
        ..writeAsStringSync('\n');
860
      processManager = FakeProcessManager.empty();
861 862
    });

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

866 867
      expect(await device.sdkNameAndVersion, equals('Fuchsia'));
    }, overrides: <Type, Generator>{
868
      ProcessManager: () => processManager,
869
      FuchsiaArtifacts: () => FuchsiaArtifacts(),
870
      FuchsiaSdk: () => FakeFuchsiaSdk(),
871 872
    });

873 874 875 876 877 878 879 880 881
    testUsingContext('returns what we get from the device on success',
        () async {
      processManager.addCommand(const FakeCommand(command: <String>[
        'ssh',
        '-F',
        '/ssh_config',
        '123',
        'cat /pkgfs/packages/build-info/0/data/version'
      ], stdout: 'version'));
882
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
883

884 885
      expect(await device.sdkNameAndVersion, equals('Fuchsia version'));
    }, overrides: <Type, Generator>{
886
      ProcessManager: () => processManager,
887
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
888
      FuchsiaSdk: () => FakeFuchsiaSdk(),
889 890 891
    });

    testUsingContext('returns "Fuchsia" when device command fails', () async {
892
      processManager.addCommand(const FakeCommand(
893 894 895 896 897 898 899
        command: <String>[
          'ssh',
          '-F',
          '/ssh_config',
          '123',
          'cat /pkgfs/packages/build-info/0/data/version'
        ],
900 901
        exitCode: 1,
      ));
902
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
903

904 905
      expect(await device.sdkNameAndVersion, equals('Fuchsia'));
    }, overrides: <Type, Generator>{
906
      ProcessManager: () => processManager,
907
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
908
      FuchsiaSdk: () => FakeFuchsiaSdk(),
909 910
    });

911 912
    testUsingContext('returns "Fuchsia" when device gives an empty result',
        () async {
913
      processManager.addCommand(const FakeCommand(
914 915 916 917 918 919 920
        command: <String>[
          'ssh',
          '-F',
          '/ssh_config',
          '123',
          'cat /pkgfs/packages/build-info/0/data/version'
        ],
921
      ));
922
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
923

924 925
      expect(await device.sdkNameAndVersion, equals('Fuchsia'));
    }, overrides: <Type, Generator>{
926
      ProcessManager: () => processManager,
927
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
928
      FuchsiaSdk: () => FakeFuchsiaSdk(),
929 930
    });
  });
931 932 933
}

class FuchsiaModulePackage extends ApplicationPackage {
934
  FuchsiaModulePackage({required this.name}) : super(id: name);
935 936 937

  @override
  final String name;
938
}
939

940 941 942
// 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
943
class MockFuchsiaDevice extends Fake implements FuchsiaDevice {
944 945 946
  MockFuchsiaDevice(this.id, this.portForwarder, this._ipv6);

  final bool _ipv6;
947 948

  @override
949
  bool get ipv6 => _ipv6;
950

951 952
  @override
  final String id;
953

954 955 956 957
  @override
  final DevicePortForwarder portForwarder;

  @override
958 959
  Future<TargetPlatform> get targetPlatform async =>
      TargetPlatform.fuchsia_arm64;
960 961

  @override
962
  String get name => 'fuchsia';
963 964

  @override
965
  Future<List<int>> servicePorts() async => <int>[1];
966 967

  @override
968
  DartDevelopmentService get dds => FakeDartDevelopmentService();
969 970
}

971 972
class FakePortForwarder extends Fake implements DevicePortForwarder {
  bool disposed = false;
973 974

  @override
975 976
  Future<void> dispose() async {
    disposed = true;
977
  }
978 979
}

980
class FakeFuchsiaFfx implements FuchsiaFfx {
981
  @override
982
  Future<List<String>> list({Duration? timeout}) async {
983
    return <String>['192.168.42.172 scare-cable-skip-ffx'];
984 985 986
  }

  @override
987 988
  Future<String> resolve(String deviceName) async {
    return '192.168.42.10';
989
  }
990 991

  @override
992
  Future<String?> sessionShow() async {
993 994 995 996 997 998 999
    return null;
  }

  @override
  Future<bool> sessionAdd(String url) async {
    return false;
  }
1000 1001
}

1002 1003 1004 1005
class FakeFuchsiaPM extends Fake implements FuchsiaPM {}

class FakeFuchsiaKernelCompiler extends Fake implements FuchsiaKernelCompiler {}

1006 1007
class FakeFuchsiaSdk extends Fake implements FuchsiaSdk {
  FakeFuchsiaSdk({
1008 1009 1010 1011 1012 1013
    FuchsiaPM? pm,
    FuchsiaKernelCompiler? compiler,
    FuchsiaFfx? ffx,
    String? devices,
  })  : fuchsiaPM = pm ?? FakeFuchsiaPM(),
        fuchsiaKernelCompiler = compiler ?? FakeFuchsiaKernelCompiler(),
1014 1015
        fuchsiaFfx = ffx ?? FakeFuchsiaFfx(),
        _devices = devices;
1016 1017

  @override
1018
  final FuchsiaPM fuchsiaPM;
1019 1020

  @override
1021
  final FuchsiaKernelCompiler fuchsiaKernelCompiler;
1022 1023

  @override
1024
  final FuchsiaFfx fuchsiaFfx;
1025

1026
  final String? _devices;
1027

1028
  @override
1029
  Future<String?> listDevices({Duration? timeout}) async {
1030
    return _devices;
1031 1032 1033
  }
}

1034 1035
class FakeDartDevelopmentService extends Fake
    implements DartDevelopmentService {
1036
  @override
1037 1038
  Future<void> startDartDevelopmentService(
    Uri observatoryUri, {
1039 1040 1041 1042
    required Logger logger,
    int? hostPort,
    bool? ipv6,
    bool? disableServiceAuthCodes,
1043
    bool cacheStartupProfile = false,
1044
  }) async {}
1045 1046

  @override
1047
  Uri get uri => Uri.parse('example');
1048 1049
}

1050 1051 1052 1053 1054 1055 1056
class FakeFuchsiaWorkflow implements FuchsiaWorkflow {
  FakeFuchsiaWorkflow({
    this.appliesToHostPlatform = true,
    this.canLaunchDevices = true,
    this.canListDevices = true,
    this.canListEmulators = true,
  });
1057 1058

  @override
1059
  bool appliesToHostPlatform;
1060

1061
  @override
1062
  bool canLaunchDevices;
1063 1064

  @override
1065
  bool canListDevices;
1066 1067

  @override
1068
  bool canListEmulators;
1069
}