fuchsia_device_test.dart 34.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
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/features.dart';
21
import 'package:flutter_tools/src/fuchsia/fuchsia_device.dart';
22
import 'package:flutter_tools/src/fuchsia/fuchsia_ffx.dart';
23 24
import 'package:flutter_tools/src/fuchsia/fuchsia_kernel_compiler.dart';
import 'package:flutter_tools/src/fuchsia/fuchsia_pm.dart';
25
import 'package:flutter_tools/src/fuchsia/fuchsia_sdk.dart';
26
import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
27
import 'package:flutter_tools/src/globals.dart' as globals;
28 29
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/vmservice.dart';
30
import 'package:test/fake.dart';
31
import 'package:vm_service/vm_service.dart' as vm_service;
32

33 34
import '../../src/common.dart';
import '../../src/context.dart';
35
import '../../src/fake_vm_services.dart';
36
import '../../src/fakes.dart';
37

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

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

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

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

73 74 75 76
      expect(device.id, deviceId);
      expect(device.name, name);
    });

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

88 89 90 91
    testWithoutContext('lists nothing when workflow cannot list devices',
        () async {
      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 106 107
      final FakeFuchsiaSdk fuchsiaSdk = FakeFuchsiaSdk(
          devices:
              '2001:0db8:85a3:0000:0000:8a2e:0370:7334 paper-pulp-bush-angel');
108
      final FuchsiaDevices fuchsiaDevices = FuchsiaDevices(
109
        platform: FakePlatform(environment: <String, String>{}),
110 111 112 113
        fuchsiaSdk: fuchsiaSdk,
        fuchsiaWorkflow: fuchsiaWorkflow,
        logger: BufferLogger.test(),
      );
114

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

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

121
    testWithoutContext('can parse ffx output for multiple devices', () async {
122
      final FakeFuchsiaWorkflow fuchsiaWorkflow = FakeFuchsiaWorkflow();
123 124 125 126
      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');
127
      final FuchsiaDevices fuchsiaDevices = FuchsiaDevices(
128
        platform: FakePlatform(),
129 130 131 132 133 134 135 136 137 138 139 140 141
        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');
    });

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

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

      expect(devices, isEmpty);
    });

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

      expect(portForwarder.disposed, true);
165 166
    });

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

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

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

183 184
      expect(device.ephemeral, true);
    });
185

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

193
      expect(device.isSupportedForProject(project), true);
194 195
    });

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

202
      expect(device.isSupportedForProject(project), false);
203
    });
204

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

351
  ''';
352 353 354
      late FakeProcessManager processManager;
      late File ffx;
      late File sshConfig;
355

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

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

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

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

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

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

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

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

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

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

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

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

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

494 495 496
      expect(device.supportsScreenshot, false);
    }, overrides: <Type, Generator>{
      Platform: () => FakePlatform(
497 498
            operatingSystem: 'windows',
          ),
499
      FeatureFlags: () => TestFeatureFlags(isFuchsiaEnabled: true),
500 501
    });

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

    testUsingContext('takeScreenshot throws if screencap failed', () async {
514 515 516 517 518 519 520
      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>'));
521
      final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester');
522 523 524

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

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

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

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

      await device.takeScreenshot(globals.fs.file('file.ppm'));
628 629
      expect(
        testLogger.errorText,
630 631
        contains(
            'Failed to delete screenshot.ppm from the device:\n<error-message>'),
632 633
      );
    }, overrides: <Type, Generator>{
634
      ProcessManager: () => processManager,
635
      FileSystem: () => MemoryFileSystem.test(),
636
      Platform: () => FakePlatform(
637 638 639 640
            environment: <String, String>{
              'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
            },
          ),
641
      FeatureFlags: () => TestFeatureFlags(isFuchsiaEnabled: true),
642 643 644 645
    }, testOn: 'posix');

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

674 675
      expect(() => device.takeScreenshot(globals.fs.file('file.ppm')),
          returnsNormally);
676
    }, overrides: <Type, Generator>{
677
      ProcessManager: () => processManager,
678
      FileSystem: () => MemoryFileSystem.test(),
679
      Platform: () => FakePlatform(
680 681 682 683
            environment: <String, String>{
              'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
            },
          ),
684
      FeatureFlags: () => TestFeatureFlags(isFuchsiaEnabled: true),
685
    });
686 687
  });

688
  group('portForwarder', () {
689 690
    late FakeProcessManager processManager;
    late File sshConfig;
691 692

    setUp(() {
693
      processManager = FakeProcessManager.empty();
694 695
      sshConfig = MemoryFileSystem.test().file('irrelevant')
        ..writeAsStringSync('\n');
696 697
    });

698 699
    testUsingContext(
        '`unforward` prints stdout and stderr if ssh command failed', () async {
700
      final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
701
      processManager.addCommand(const FakeCommand(
702 703 704 705 706 707 708 709 710 711 712
        command: <String>[
          'ssh',
          '-F',
          '/irrelevant',
          '-O',
          'cancel',
          '-vvv',
          '-L',
          '0:127.0.0.1:1',
          'id'
        ],
713 714 715 716
        exitCode: 1,
        stdout: '<stdout>',
        stderr: '<stderr>',
      ));
717 718

      await expectLater(
719 720 721 722 723
        () => device.portForwarder
            .unforward(ForwardedPort(/*hostPort=*/ 0, /*devicePort=*/ 1)),
        throwsToolExit(
            message:
                'Unforward command failed:\nstdout: <stdout>\nstderr: <stderr>'),
724 725
      );
    }, overrides: <Type, Generator>{
726
      ProcessManager: () => processManager,
727 728 729 730
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
    });
  });

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

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

784 785
      expect(
          uri.toString(), 'http://${InternetAddress.loopbackIPv4.address}:0/');
786 787
    });

788 789
    testUsingContext('can handle flutter view without matching isolate name',
        () async {
790
      const String expectedIsolateName = 'foobar';
791 792
      final Future<Uri> uri = findUri(<FlutterView>[
        // no ui isolate.
793
        FlutterView(id: '1', uiIsolate: fakeIsolate),
794
        // wrong name.
795 796
        FlutterView(
            id: '2',
797
            uiIsolate: vm_service.Isolate.parse(<String, Object?>{
798 799 800
              ...fakeIsolate.toJson(),
              'name': 'wrong name',
            })),
801
      ], expectedIsolateName);
802

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

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

812 813 814
      expect(uri, throwsException);
    });
  });
815

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

861
  group('sdkNameAndVersion: ', () {
862 863
    late File sshConfig;
    late FakeProcessManager processManager;
864 865

    setUp(() {
866 867
      sshConfig = MemoryFileSystem.test().file('ssh_config')
        ..writeAsStringSync('\n');
868
      processManager = FakeProcessManager.empty();
869 870
    });

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

874 875
      expect(await device.sdkNameAndVersion, equals('Fuchsia'));
    }, overrides: <Type, Generator>{
876
      ProcessManager: () => processManager,
877
      FuchsiaArtifacts: () => FuchsiaArtifacts(),
878
      FuchsiaSdk: () => FakeFuchsiaSdk(),
879 880
    });

881 882 883 884 885 886 887 888 889
    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'));
890
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
891

892 893
      expect(await device.sdkNameAndVersion, equals('Fuchsia version'));
    }, overrides: <Type, Generator>{
894
      ProcessManager: () => processManager,
895
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
896
      FuchsiaSdk: () => FakeFuchsiaSdk(),
897 898 899
    });

    testUsingContext('returns "Fuchsia" when device command fails', () async {
900
      processManager.addCommand(const FakeCommand(
901 902 903 904 905 906 907
        command: <String>[
          'ssh',
          '-F',
          '/ssh_config',
          '123',
          'cat /pkgfs/packages/build-info/0/data/version'
        ],
908 909
        exitCode: 1,
      ));
910
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
911

912 913
      expect(await device.sdkNameAndVersion, equals('Fuchsia'));
    }, overrides: <Type, Generator>{
914
      ProcessManager: () => processManager,
915
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
916
      FuchsiaSdk: () => FakeFuchsiaSdk(),
917 918
    });

919 920
    testUsingContext('returns "Fuchsia" when device gives an empty result',
        () async {
921
      processManager.addCommand(const FakeCommand(
922 923 924 925 926 927 928
        command: <String>[
          'ssh',
          '-F',
          '/ssh_config',
          '123',
          'cat /pkgfs/packages/build-info/0/data/version'
        ],
929
      ));
930
      final FuchsiaDevice device = FuchsiaDevice('123', name: 'device');
931

932 933
      expect(await device.sdkNameAndVersion, equals('Fuchsia'));
    }, overrides: <Type, Generator>{
934
      ProcessManager: () => processManager,
935
      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
936
      FuchsiaSdk: () => FakeFuchsiaSdk(),
937 938
    });
  });
939 940 941
}

class FuchsiaModulePackage extends ApplicationPackage {
942
  FuchsiaModulePackage({required this.name}) : super(id: name);
943 944 945

  @override
  final String name;
946
}
947

948 949 950
// 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
951
class MockFuchsiaDevice extends Fake implements FuchsiaDevice {
952 953 954
  MockFuchsiaDevice(this.id, this.portForwarder, this._ipv6);

  final bool _ipv6;
955 956

  @override
957
  bool get ipv6 => _ipv6;
958

959 960
  @override
  final String id;
961

962 963 964 965
  @override
  final DevicePortForwarder portForwarder;

  @override
966 967
  Future<TargetPlatform> get targetPlatform async =>
      TargetPlatform.fuchsia_arm64;
968 969

  @override
970
  String get name => 'fuchsia';
971 972

  @override
973
  Future<List<int>> servicePorts() async => <int>[1];
974 975

  @override
976
  DartDevelopmentService get dds => FakeDartDevelopmentService();
977 978
}

979 980
class FakePortForwarder extends Fake implements DevicePortForwarder {
  bool disposed = false;
981 982

  @override
983 984
  Future<void> dispose() async {
    disposed = true;
985
  }
986 987
}

988
class FakeFuchsiaFfx implements FuchsiaFfx {
989
  @override
990
  Future<List<String>> list({Duration? timeout}) async {
991
    return <String>['192.168.42.172 scare-cable-skip-ffx'];
992 993 994
  }

  @override
995 996
  Future<String> resolve(String deviceName) async {
    return '192.168.42.10';
997
  }
998 999

  @override
1000
  Future<String?> sessionShow() async {
1001 1002 1003 1004 1005 1006 1007
    return null;
  }

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

1010 1011 1012 1013
class FakeFuchsiaPM extends Fake implements FuchsiaPM {}

class FakeFuchsiaKernelCompiler extends Fake implements FuchsiaKernelCompiler {}

1014 1015
class FakeFuchsiaSdk extends Fake implements FuchsiaSdk {
  FakeFuchsiaSdk({
1016 1017 1018 1019 1020 1021
    FuchsiaPM? pm,
    FuchsiaKernelCompiler? compiler,
    FuchsiaFfx? ffx,
    String? devices,
  })  : fuchsiaPM = pm ?? FakeFuchsiaPM(),
        fuchsiaKernelCompiler = compiler ?? FakeFuchsiaKernelCompiler(),
1022 1023
        fuchsiaFfx = ffx ?? FakeFuchsiaFfx(),
        _devices = devices;
1024 1025

  @override
1026
  final FuchsiaPM fuchsiaPM;
1027 1028

  @override
1029
  final FuchsiaKernelCompiler fuchsiaKernelCompiler;
1030 1031

  @override
1032
  final FuchsiaFfx fuchsiaFfx;
1033

1034
  final String? _devices;
1035

1036
  @override
1037
  Future<String?> listDevices({Duration? timeout}) async {
1038
    return _devices;
1039 1040 1041
  }
}

1042 1043
class FakeDartDevelopmentService extends Fake
    implements DartDevelopmentService {
1044
  @override
1045
  Future<void> startDartDevelopmentService(
1046
    Uri vmServiceUri, {
1047 1048 1049 1050
    required Logger logger,
    int? hostPort,
    bool? ipv6,
    bool? disableServiceAuthCodes,
1051
    bool cacheStartupProfile = false,
1052
  }) async {}
1053 1054

  @override
1055
  Uri get uri => Uri.parse('example');
1056 1057
}

1058 1059 1060 1061 1062 1063 1064
class FakeFuchsiaWorkflow implements FuchsiaWorkflow {
  FakeFuchsiaWorkflow({
    this.appliesToHostPlatform = true,
    this.canLaunchDevices = true,
    this.canListDevices = true,
    this.canListEmulators = true,
  });
1065 1066

  @override
1067
  bool appliesToHostPlatform;
1068

1069
  @override
1070
  bool canLaunchDevices;
1071 1072

  @override
1073
  bool canListDevices;
1074 1075

  @override
1076
  bool canListEmulators;
1077
}