Implement takeScreenshot and add driver test for Fuchsia (#48611)

diff --git a/dev/devicelab/bin/tasks/flutter_driver_screenshot_test_fuchsia.dart b/dev/devicelab/bin/tasks/flutter_driver_screenshot_test_fuchsia.dart
new file mode 100644
index 0000000..8d97bb9
--- /dev/null
+++ b/dev/devicelab/bin/tasks/flutter_driver_screenshot_test_fuchsia.dart
@@ -0,0 +1,14 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter_devicelab/framework/adb.dart';
+import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/tasks/integration_tests.dart';
+
+Future<void> main() async {
+  deviceOperatingSystem = DeviceOperatingSystem.fuchsia;
+  await task(createFlutterDriverScreenshotTest());
+}
diff --git a/dev/integration_tests/flutter_driver_screenshot_test/fuchsia/meta/flutter_driver_screenshot_test.cmx b/dev/integration_tests/flutter_driver_screenshot_test/fuchsia/meta/flutter_driver_screenshot_test.cmx
new file mode 100644
index 0000000..2938f45
--- /dev/null
+++ b/dev/integration_tests/flutter_driver_screenshot_test/fuchsia/meta/flutter_driver_screenshot_test.cmx
@@ -0,0 +1,22 @@
+{
+    "program": {
+        "data": "data/flutter_driver_screenshot_test"
+    },
+    "sandbox": {
+        "services": [
+            "fuchsia.cobalt.LoggerFactory",
+            "fuchsia.fonts.Provider",
+            "fuchsia.logger.LogSink",
+            "fuchsia.modular.Clipboard",
+            "fuchsia.modular.ContextWriter",
+            "fuchsia.modular.DeviceMap",
+            "fuchsia.modular.ModuleContext",
+            "fuchsia.sys.Environment",
+            "fuchsia.sys.Launcher",
+            "fuchsia.testing.runner.TestRunner",
+            "fuchsia.ui.input.ImeService",
+            "fuchsia.ui.policy.Presenter",
+            "fuchsia.ui.scenic.Scenic"
+        ]
+    }
+}
diff --git a/dev/integration_tests/flutter_driver_screenshot_test/lib/main.dart b/dev/integration_tests/flutter_driver_screenshot_test/lib/main.dart
index 9147d16..1757908 100644
--- a/dev/integration_tests/flutter_driver_screenshot_test/lib/main.dart
+++ b/dev/integration_tests/flutter_driver_screenshot_test/lib/main.dart
@@ -71,28 +71,20 @@
   Future<String> _handleDriverMessage(String message) async {
     switch (message) {
       case 'device_model':
-        String target;
+        final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
         switch (Theme.of(context).platform) {
           case TargetPlatform.iOS:
-            target = 'ios';
-            break;
-          case TargetPlatform.android:
-            target = 'android';
-            break;
-          default:
-            target = 'unsupported';
-            break;
-        }
-        final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
-        if (target == 'ios') {
-          final IosDeviceInfo iosDeviceInfo = await deviceInfo.iosInfo;
-          if (iosDeviceInfo.isPhysicalDevice) {
-            return iosDeviceInfo.utsname.machine;
-          } else {
+            final IosDeviceInfo iosDeviceInfo = await deviceInfo.iosInfo;
+            if (iosDeviceInfo.isPhysicalDevice) {
+              return iosDeviceInfo.utsname.machine;
+            }
             return 'sim_' + iosDeviceInfo.name;
-          }
-        } else if (target == 'android') {
-          return (await deviceInfo.androidInfo).model;
+          case TargetPlatform.android:
+            return (await deviceInfo.androidInfo).model;
+          case TargetPlatform.fuchsia:
+            return 'fuchsia';
+          default:
+            return 'unsupported';
         }
         break;
     }
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
index 7541049..644bbb0 100644
--- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
@@ -139,7 +139,7 @@
   FuchsiaDevices() : super('Fuchsia devices');
 
   @override
-  bool get supportsPlatform => globals.platform.isLinux || globals.platform.isMacOS;
+  bool get supportsPlatform => isFuchsiaSupportedPlatform();
 
   @override
   bool get canListAnything => fuchsiaWorkflow.canListDevices;
@@ -443,6 +443,39 @@
   }
 
   @override
+  bool get supportsScreenshot => isFuchsiaSupportedPlatform();
+
+  @override
+  Future<void> takeScreenshot(File outputFile) async {
+    if (outputFile.basename.split('.').last != 'ppm') {
+      throw '${outputFile.path} must be a .ppm file';
+    }
+    final RunResult screencapResult = await shell('screencap > /tmp/screenshot.ppm');
+    if (screencapResult.exitCode != 0) {
+      throw 'Could not take a screenshot on device $name:\n$screencapResult';
+    }
+    try {
+      final RunResult scpResult =  await scp('/tmp/screenshot.ppm', outputFile.path);
+      if (scpResult.exitCode != 0) {
+        throw 'Failed to copy screenshot from device:\n$scpResult';
+      }
+    } finally {
+      try {
+        final RunResult deleteResult = await shell('rm /tmp/screenshot.ppm');
+        if (deleteResult.exitCode != 0) {
+          globals.printError(
+            'Failed to delete screenshot.ppm from the device:\n$deleteResult'
+          );
+        }
+      } catch (_) {
+        globals.printError(
+          'Failed to delete screenshot.ppm from the device'
+        );
+      }
+    }
+  }
+
+  @override
   Future<TargetPlatform> get targetPlatform async => _targetPlatform ??= await _queryTargetPlatform();
 
   @override
@@ -479,9 +512,6 @@
   @override
   void clearLogs() {}
 
-  @override
-  bool get supportsScreenshot => false;
-
   bool get ipv6 {
     try {
       Uri.parseIPv6Address(id);
@@ -545,6 +575,21 @@
     ]);
   }
 
+  /// Transfer the file [origin] from the device to [destination].
+  Future<RunResult> scp(String origin, String destination) async {
+    if (fuchsiaArtifacts.sshConfig == null) {
+      throwToolExit('Cannot interact with device. No ssh config.\n'
+                    'Try setting FUCHSIA_SSH_CONFIG or FUCHSIA_BUILD_DIR.');
+    }
+    return await processUtils.run(<String>[
+      'scp',
+      '-F',
+      fuchsiaArtifacts.sshConfig.absolute.path,
+      '$id:$origin',
+      destination,
+    ]);
+  }
+
   /// Finds the first port running a VM matching `isolateName` from the
   /// provided set of `ports`.
   ///
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart
index b5effe8..628e624 100644
--- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart
@@ -20,6 +20,11 @@
 /// The [FuchsiaArtifacts] instance.
 FuchsiaArtifacts get fuchsiaArtifacts => context.get<FuchsiaArtifacts>();
 
+/// Returns [true] if the current platform supports Fuchsia targets.
+bool isFuchsiaSupportedPlatform() {
+  return globals.platform.isLinux || globals.platform.isMacOS;
+}
+
 /// The Fuchsia SDK shell commands.
 ///
 /// This workflow assumes development within the fuchsia source tree,
@@ -110,7 +115,7 @@
   /// FUCHSIA_SSH_CONFIG) to find the ssh configuration needed to talk to
   /// a device.
   factory FuchsiaArtifacts.find() {
-    if (!globals.platform.isLinux && !globals.platform.isMacOS) {
+    if (!isFuchsiaSupportedPlatform()) {
       // Don't try to find the artifacts on platforms that are not supported.
       return FuchsiaArtifacts();
     }
diff --git a/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_device_test.dart b/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_device_test.dart
index 6f6127e..2810fcf 100644
--- a/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_device_test.dart
+++ b/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_device_test.dart
@@ -31,6 +31,7 @@
 import 'package:meta/meta.dart';
 import 'package:mockito/mockito.dart';
 import 'package:process/process.dart';
+import 'package:platform/platform.dart';
 
 import '../../src/common.dart';
 import '../../src/context.dart';
@@ -325,6 +326,230 @@
     });
   });
 
+   group('screenshot', () {
+      MockProcessManager mockProcessManager;
+
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+    });
+
+    test('is supported on posix platforms', () {
+      final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
+      expect(device.supportsScreenshot, true);
+    }, testOn: 'posix');
+
+    testUsingContext('is not supported on Windows', () {
+      final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
+      expect(device.supportsScreenshot, false);
+    }, overrides: <Type, Generator>{
+      Platform: () => FakePlatform(
+        operatingSystem: 'windows',
+      ),
+    });
+
+    test('takeScreenshot throws if file isn\'t .ppm', () async {
+      final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
+      await expectLater(
+        () => device.takeScreenshot(globals.fs.file('file.invalid')),
+        throwsA(equals('file.invalid must be a .ppm file')),
+      );
+    }, testOn: 'posix');
+
+    testUsingContext('takeScreenshot throws if screencap failed', () async {
+      final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester');
+
+      when(mockProcessManager.run(
+        const <String>[
+          'ssh',
+          '-F',
+          '/fuchsia/out/default/.ssh',
+          '0.0.0.0',
+          'screencap > /tmp/screenshot.ppm',
+        ],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) async => ProcessResult(0, 1, '', '<error-message>'));
+
+      await expectLater(
+        () => device.takeScreenshot(globals.fs.file('file.ppm')),
+        throwsA(equals('Could not take a screenshot on device tester:\n<error-message>')),
+      );
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Platform: () => FakePlatform(
+        environment: <String, String>{
+          'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
+        },
+        operatingSystem: 'linux',
+      ),
+    }, testOn: 'posix');
+
+    testUsingContext('takeScreenshot throws if scp failed', () async {
+      final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester');
+
+      when(mockProcessManager.run(
+        const <String>[
+          'ssh',
+          '-F',
+          '/fuchsia/out/default/.ssh',
+          '0.0.0.0',
+          'screencap > /tmp/screenshot.ppm',
+        ],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) async => ProcessResult(0, 0, '', ''));
+
+      when(mockProcessManager.run(
+        const <String>[
+          'scp',
+          '-F',
+          '/fuchsia/out/default/.ssh',
+          '0.0.0.0:/tmp/screenshot.ppm',
+          'file.ppm',
+        ],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) async => ProcessResult(0, 1, '', '<error-message>'));
+
+       when(mockProcessManager.run(
+        const <String>[
+          'ssh',
+          '-F',
+          '/fuchsia/out/default/.ssh',
+          '0.0.0.0',
+          'rm /tmp/screenshot.ppm',
+        ],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) async => ProcessResult(0, 0, '', ''));
+
+      await expectLater(
+        () => device.takeScreenshot(globals.fs.file('file.ppm')),
+        throwsA(equals('Failed to copy screenshot from device:\n<error-message>')),
+      );
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Platform: () => FakePlatform(
+        environment: <String, String>{
+          'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
+        },
+        operatingSystem: 'linux',
+      ),
+    }, testOn: 'posix');
+
+    testUsingContext('takeScreenshot prints error if can\'t delete file from device', () async {
+      final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester');
+
+      when(mockProcessManager.run(
+        const <String>[
+          'ssh',
+          '-F',
+          '/fuchsia/out/default/.ssh',
+          '0.0.0.0',
+          'screencap > /tmp/screenshot.ppm',
+        ],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) async => ProcessResult(0, 0, '', ''));
+
+      when(mockProcessManager.run(
+        const <String>[
+          'scp',
+          '-F',
+          '/fuchsia/out/default/.ssh',
+          '0.0.0.0:/tmp/screenshot.ppm',
+          'file.ppm',
+        ],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) async => ProcessResult(0, 0, '', ''));
+
+       when(mockProcessManager.run(
+        const <String>[
+          'ssh',
+          '-F',
+          '/fuchsia/out/default/.ssh',
+          '0.0.0.0',
+          'rm /tmp/screenshot.ppm',
+        ],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) async => ProcessResult(0, 1, '', '<error-message>'));
+
+      try {
+        await device.takeScreenshot(globals.fs.file('file.ppm'));
+      } catch (_) {
+        assert(false);
+      }
+      expect(
+        testLogger.errorText,
+        contains('Failed to delete screenshot.ppm from the device:\n<error-message>'),
+      );
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Platform: () => FakePlatform(
+        environment: <String, String>{
+          'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
+        },
+        operatingSystem: 'linux',
+      ),
+    }, testOn: 'posix');
+
+    testUsingContext('takeScreenshot returns', () async {
+      final FuchsiaDevice device = FuchsiaDevice('0.0.0.0', name: 'tester');
+
+      when(mockProcessManager.run(
+        const <String>[
+          'ssh',
+          '-F',
+          '/fuchsia/out/default/.ssh',
+          '0.0.0.0',
+          'screencap > /tmp/screenshot.ppm',
+        ],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) async => ProcessResult(0, 0, '', ''));
+
+      when(mockProcessManager.run(
+        const <String>[
+          'scp',
+          '-F',
+          '/fuchsia/out/default/.ssh',
+          '0.0.0.0:/tmp/screenshot.ppm',
+          'file.ppm',
+        ],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) async => ProcessResult(0, 0, '', ''));
+
+       when(mockProcessManager.run(
+        const <String>[
+          'ssh',
+          '-F',
+          '/fuchsia/out/default/.ssh',
+          '0.0.0.0',
+          'rm /tmp/screenshot.ppm',
+        ],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) async => ProcessResult(0, 0, '', ''));
+
+      try {
+        await device.takeScreenshot(globals.fs.file('file.ppm'));
+      } catch (_) {
+        assert(false);
+      }
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Platform: () => FakePlatform(
+        environment: <String, String>{
+          'FUCHSIA_SSH_CONFIG': '/fuchsia/out/default/.ssh',
+        },
+        operatingSystem: 'linux',
+      ),
+    }, testOn: 'posix');
+  });
+
   group(FuchsiaIsolateDiscoveryProtocol, () {
     MockPortForwarder portForwarder;
     MockVMService vmService;