Skip to content

Commit 0946192

Browse files
authored
Scaffolding for NativeDriver and AndroidNativeDriver for taking screenshots using adb. (flutter#152194)
Closes flutter#152189. I have next to no clue how to configure this to run on CI, so bear with me as I rediscover the wheel.
1 parent 00257e8 commit 0946192

File tree

9 files changed

+332
-0
lines changed

9 files changed

+332
-0
lines changed

.ci.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,6 +1232,15 @@ targets:
12321232
- bin/**
12331233
- .ci.yaml
12341234

1235+
- name: Linux_android_emu flutter_driver_android_test
1236+
recipe: flutter/flutter_drone
1237+
timeout: 60
1238+
bringup: true
1239+
properties:
1240+
shard: flutter_driver_android
1241+
tags: >
1242+
["framework", "hostonly", "shard", "linux"]
1243+
12351244
- name: Linux realm_checker
12361245
recipe: flutter/flutter_drone
12371246
timeout: 60

TESTOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@
324324
# coverage @goderbauer @flutter/infra
325325
# customer_testing @Piinks @flutter/framework
326326
# docs @Piinks @flutter/framework
327+
# flutter_driver_android_test @matanlurey @johnmccutchan
327328
# flutter_packaging @christopherfujino @flutter/infra
328329
# flutter_plugins @stuartmorgan @flutter/plugin
329330
# framework_tests @Piinks @flutter/framework
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:path/path.dart' as path;
6+
import '../utils.dart';
7+
8+
Future<void> runFlutterDriverAndroidTests() async {
9+
print('Running Flutter Driver Android tests...');
10+
11+
await runDartTest(
12+
path.join(flutterRoot, 'packages', 'flutter_driver'),
13+
testPaths: <String>[
14+
'test/src/native_tests/android',
15+
],
16+
);
17+
}

dev/bots/test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import 'suite_runners/run_android_java11_integration_tool_tests.dart';
6363
import 'suite_runners/run_android_preview_integration_tool_tests.dart';
6464
import 'suite_runners/run_customer_testing_tests.dart';
6565
import 'suite_runners/run_docs_tests.dart';
66+
import 'suite_runners/run_flutter_driver_android_tests.dart';
6667
import 'suite_runners/run_flutter_packages_tests.dart';
6768
import 'suite_runners/run_framework_coverage_tests.dart';
6869
import 'suite_runners/run_framework_tests.dart';
@@ -142,6 +143,7 @@ Future<void> main(List<String> args) async {
142143
'web_skwasm_tests': webTestsSuite.runWebSkwasmUnitTests,
143144
// All web integration tests
144145
'web_long_running_tests': webTestsSuite.webLongRunningTestsRunner,
146+
'flutter_driver_android': runFlutterDriverAndroidTests,
145147
'flutter_plugins': flutterPackagesRunner,
146148
'skp_generator': skpGeneratorTestsRunner,
147149
'realm_checker': realmCheckerTestRunner,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Flutter Native Driver
2+
3+
An experiment in adding platform-aware functionality to `flutter_driver`.
4+
5+
Project tracking: <https://github.com/orgs/flutter/projects/154>.
6+
7+
We'd like to be able to test, within `flutter/flutter` (and friends):
8+
9+
- Does a web-view load and render the expected content?
10+
- Unexpected changes with the native OS, i.e. Android edge-to-edge
11+
- Impeller rendering on Android using a real GPU (not swift_shader or Skia)
12+
- Does an app correctly respond to application backgrounding and resume?
13+
- Interact with native UI elements (not rendered by Flutter) and observe output
14+
- Native text/keyboard input (IMEs, virtual keyboards, anything a11y related)
15+
16+
This project is tracking augmenting `flutter_driver` towards these goals.
17+
18+
If the project is not successful, the experiment will be turned-down and the
19+
code removed or repurposed.
20+
21+
---
22+
23+
_Questions?_ Ask in the `#hackers-tests` channel on the Flutter Discord or
24+
`@matanlurey` or `@johnmccutchan` on GitHub.
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// Examples can assume:
6+
// import 'package:flutter_driver/src/native/android.dart';
7+
8+
import 'dart:io' as io;
9+
import 'dart:typed_data';
10+
11+
import 'package:meta/meta.dart';
12+
import 'package:path/path.dart' as p;
13+
14+
import 'driver.dart';
15+
16+
/// Drives an Android device or emulator that is running a Flutter application.
17+
final class AndroidNativeDriver implements NativeDriver {
18+
/// Creates a new Android native driver with the provided configuration.
19+
///
20+
/// The [tempDirectory] argument can be used to specify a custom directory
21+
/// where the driver will store temporary files. If not provided, a temporary
22+
/// directory will be created in the system's temporary directory.
23+
@visibleForTesting
24+
AndroidNativeDriver({
25+
required AndroidDeviceTarget target,
26+
String? adbPath,
27+
io.Directory? tempDirectory,
28+
}) : _adbPath = adbPath ?? 'adb',
29+
_target = target,
30+
_tmpDir = tempDirectory ?? io.Directory.systemTemp.createTempSync('flutter_driver.');
31+
32+
/// Connects to a device or emulator identified by [target].
33+
static Future<AndroidNativeDriver> connect({
34+
AndroidDeviceTarget target = const AndroidDeviceTarget.onlyEmulatorOrDevice(),
35+
}) async {
36+
final AndroidNativeDriver driver = AndroidNativeDriver(target: target);
37+
await driver._smokeTest();
38+
return driver;
39+
}
40+
41+
Future<void> _smokeTest() async {
42+
final io.ProcessResult version = await io.Process.run(
43+
_adbPath,
44+
const <String>['version'],
45+
);
46+
if (version.exitCode != 0) {
47+
throw StateError('Failed to run `$_adbPath version`: ${version.stderr}');
48+
}
49+
50+
final io.ProcessResult devices = await io.Process.run(
51+
_adbPath,
52+
<String>[
53+
..._target._toAdbArgs(),
54+
'shell',
55+
'echo',
56+
'connected',
57+
],
58+
);
59+
if (devices.exitCode != 0) {
60+
throw StateError('Failed to connect to target: ${devices.stderr}');
61+
}
62+
}
63+
64+
final String _adbPath;
65+
final AndroidDeviceTarget _target;
66+
final io.Directory _tmpDir;
67+
68+
@override
69+
Future<void> close() async {
70+
await _tmpDir.delete(recursive: true);
71+
}
72+
73+
@override
74+
Future<NativeScreenshot> screenshot() async {
75+
final io.ProcessResult result = await io.Process.run(
76+
_adbPath,
77+
<String>[
78+
..._target._toAdbArgs(),
79+
'exec-out',
80+
'screencap',
81+
'-p',
82+
],
83+
stdoutEncoding: null,
84+
);
85+
86+
if (result.exitCode != 0) {
87+
throw StateError('Failed to take screenshot: ${result.stderr}');
88+
}
89+
90+
final Uint8List bytes = result.stdout as Uint8List;
91+
return _AdbScreencap(bytes, _tmpDir);
92+
}
93+
}
94+
95+
final class _AdbScreencap implements NativeScreenshot {
96+
const _AdbScreencap(this._bytes, this._tmpDir);
97+
98+
/// Raw bytes of the screenshot in PNG format.
99+
final Uint8List _bytes;
100+
101+
/// Temporary directory to default to when saving the screenshot.
102+
final io.Directory _tmpDir;
103+
104+
static int _lastScreenshotId = 0;
105+
106+
@override
107+
Future<String> saveAs([String? path]) async {
108+
final int id = _lastScreenshotId++;
109+
path ??= p.join(_tmpDir.path, '$id.png');
110+
await io.File(path).writeAsBytes(_bytes);
111+
return path;
112+
}
113+
114+
@override
115+
Future<Uint8List> readAsBytes() async => _bytes;
116+
}
117+
118+
/// Represents a target device running Android.
119+
sealed class AndroidDeviceTarget {
120+
/// Represents a device with the given [serialNumber].
121+
///
122+
/// This is the recommended way to target a specific device, and uses the
123+
/// device's serial number, as reported by `adb devices`, to identify the
124+
/// device:
125+
///
126+
/// ```sh
127+
/// $ adb devices
128+
/// List of devices attached
129+
/// emulator-5554 device
130+
/// ```
131+
///
132+
/// In this example, the serial number is `emulator-5554`:
133+
///
134+
/// ```dart
135+
/// const AndroidDeviceTarget target = AndroidDeviceTarget.bySerial('emulator-5554');
136+
/// ```
137+
const factory AndroidDeviceTarget.bySerial(String serialNumber) = _SerialDeviceTarget;
138+
139+
/// Represents the only running emulator _or_ connected device.
140+
///
141+
/// This is equivalent to using `adb` without `-e`, `-d`, or `-s`.
142+
const factory AndroidDeviceTarget.onlyEmulatorOrDevice() = _SingleAnyTarget;
143+
144+
/// Represents the only running emulator on the host machine.
145+
///
146+
/// This is equivalent to using `adb -e`, a _single_ emulator must be running.
147+
const factory AndroidDeviceTarget.onlyRunningEmulator() = _SingleEmulatorTarget;
148+
149+
/// Represents the only connected device on the host machine.
150+
///
151+
/// This is equivalent to using `adb -d`, a _single_ device must be connected.
152+
const factory AndroidDeviceTarget.onlyConnectedDevice() = _SingleDeviceTarget;
153+
154+
/// Returns the arguments to pass to `adb` to target this device.
155+
List<String> _toAdbArgs();
156+
}
157+
158+
final class _SerialDeviceTarget implements AndroidDeviceTarget {
159+
const _SerialDeviceTarget(this.serialNumber);
160+
final String serialNumber;
161+
162+
@override
163+
List<String> _toAdbArgs() => <String>['-s', serialNumber];
164+
}
165+
166+
final class _SingleEmulatorTarget implements AndroidDeviceTarget {
167+
const _SingleEmulatorTarget();
168+
169+
@override
170+
List<String> _toAdbArgs() => const <String>['-e'];
171+
}
172+
173+
final class _SingleDeviceTarget implements AndroidDeviceTarget {
174+
const _SingleDeviceTarget();
175+
176+
@override
177+
List<String> _toAdbArgs() => const <String>['-d'];
178+
}
179+
180+
final class _SingleAnyTarget implements AndroidDeviceTarget {
181+
const _SingleAnyTarget();
182+
183+
@override
184+
List<String> _toAdbArgs() => const <String>[];
185+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
/// @docImport 'package:flutter_driver/flutter_driver.dart';
6+
library;
7+
8+
import 'dart:typed_data';
9+
10+
/// Drives a native device or emulator that is running a Flutter application.
11+
///
12+
/// Unlike [FlutterDriver], a [NativeDriver] is backed by a platform specific
13+
/// implementation that might interact with out-of-process services, such as
14+
/// `adb` for Android or `ios-deploy` for iOS, and might require additional
15+
/// setup (e.g., adding test-only plugins to the application under test) for
16+
/// full functionality.
17+
///
18+
/// API that is available directly on [NativeDriver] is considered _lowest
19+
/// common denominator_ and is guaranteed to work on all platforms supported by
20+
/// Flutter Driver unless otherwise noted. Platform-specific functionality that
21+
/// _cannot_ be exposed through this interface is available through
22+
/// platform-specific extensions.
23+
abstract interface class NativeDriver {
24+
/// Closes the native driver and releases any resources associated with it.
25+
///
26+
/// After calling this method, the driver is no longer usable.
27+
Future<void> close();
28+
29+
/// Take a screenshot using a platform-specific mechanism.
30+
///
31+
/// The image is returned as an opaque handle that can be used to retrieve
32+
/// the screenshot data or to compare it with another screenshot, and may
33+
/// include platform-specific system UI elements, such as the status bar or
34+
/// navigation bar.
35+
Future<NativeScreenshot> screenshot();
36+
}
37+
38+
/// An opaque handle to a screenshot taken on a native device.
39+
///
40+
/// Unlike [FlutterDriver.screenshot], the screenshot represented by this handle
41+
/// is generated by a platform-specific mechanism and is often already stored
42+
/// on disk. The handle can be used to retrieve the screenshot data or to
43+
/// compare it with another screenshot.
44+
abstract interface class NativeScreenshot {
45+
/// Saves the screenshot to a file at the specified [path].
46+
///
47+
/// If [path] is not provided, a temporary file will be created.
48+
///
49+
/// Returns the path to the saved file.
50+
Future<String> saveAs([String? path]);
51+
52+
/// Reads the screenshot as a PNG-formatted list of bytes.
53+
Future<Uint8List> readAsBytes();
54+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# `AndroidNativeDriver` Tests
2+
3+
This directory are tests that require an Android device or emulator to run.
4+
5+
To run locally, connect an Android device or start an emulator and run:
6+
7+
```bash
8+
# Assumuing your current working directory is `packages/flutter_driver`.\
9+
10+
$ flutter test test/src/native_tests/android
11+
```
12+
13+
On CI, these tests are run via [`run_flutter_driver_android_tests.dart`][ci].
14+
15+
[ci]: ../../../../../../dev/bots/suite_runners/run_flutter_driver_android_tests.dart
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:io' as io;
6+
import 'dart:typed_data';
7+
8+
import 'package:flutter_driver/src/native/android.dart';
9+
import 'package:flutter_driver/src/native/driver.dart';
10+
import 'package:test/test.dart';
11+
12+
void main() async {
13+
test('should connect to an Android device and take a screenshot', () async {
14+
final NativeDriver driver = await AndroidNativeDriver.connect();
15+
final NativeScreenshot screenshot = await driver.screenshot();
16+
17+
final Uint8List bytes = await screenshot.readAsBytes();
18+
expect(bytes.length, greaterThan(0));
19+
20+
final String path = await screenshot.saveAs();
21+
expect(io.File(path).readAsBytesSync(), bytes);
22+
23+
await driver.close();
24+
});
25+
}

0 commit comments

Comments
 (0)