Skip to content

Commit 01fa07c

Browse files
author
Emmanuel Garcia
committed
Revert "Revert "Re-enable scenario tests on Android (flutter#33574)" (flutter#33813)"
This reverts commit 0de2f6a.
1 parent dff8bd9 commit 01fa07c

File tree

58 files changed

+722
-329
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+722
-329
lines changed

.ci.yaml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,24 @@ targets:
100100
timeout: 60
101101

102102
- name: Linux Android Emulator Tests
103-
bringup: true # Recipe issue https://github.com/flutter/flutter/issues/86427
104103
recipe: engine/scenarios
104+
enabled_branches:
105+
- main
106+
- master
105107
properties:
106108
dependencies: >-
107109
[
108-
{"dependency": "android_virtual_device", "version": "31"}
110+
{"dependency": "android_virtual_device", "version": "31"},
111+
{"dependency": "goldctl"}
109112
]
110113
upload_packages: "true"
111114
clobber: "true"
112115
timeout: 60
116+
runIf:
117+
- DEPS
118+
- .ci.yaml
119+
- testing/**
120+
- shell/platforms/android/**
113121

114122
- name: Linux Benchmarks
115123
enabled_branches:

shell/platform/android/android_context_gl_unittests.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ TEST(AndroidSurfaceGL, CreateSnapshopSurfaceWhenOnscreenSurfaceIsNull) {
164164
EXPECT_NE(android_surface->GetOnscreenSurface(), nullptr);
165165
}
166166

167-
TEST(AndroidContextGl, MSAAx4) {
167+
// TODO(https://github.com/flutter/flutter/issues/104463): Flaky test.
168+
TEST(AndroidContextGl, DISABLED_MSAAx4) {
168169
GrMockOptions main_context_options;
169170
sk_sp<GrDirectContext> main_context =
170171
GrDirectContext::MakeMock(&main_context_options);

shell/platform/android/external_view_embedder/external_view_embedder.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ void AndroidExternalViewEmbedder::SubmitFrame(
9898

9999
for (size_t i = 0; i < current_frame_view_count; i++) {
100100
int64_t view_id = composition_order_[i];
101+
if (picture_recorders_.at(view_id)->getRecordingCanvas() == nullptr) {
102+
continue;
103+
}
101104

102105
sk_sp<SkPicture> picture =
103106
picture_recorders_.at(view_id)->finishRecordingAsPicture();

testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/ScreenshotUtil.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ private static class Connection {
3737
}
3838

3939
synchronized void writeFile(String name, byte[] fileContent) throws IOException {
40-
final ByteBuffer buffer = ByteBuffer.allocate(name.length() + fileContent.length + 4);
40+
final ByteBuffer buffer = ByteBuffer.allocate(name.length() + fileContent.length + 8);
41+
// See ScreenshotBlobTransformer#bind in screenshot_transformer.dart for consumer side.
4142
buffer.putInt(name.length());
43+
buffer.putInt(fileContent.length);
4244
buffer.put(name.getBytes());
4345
buffer.put(fileContent);
4446
final byte[] bytes = buffer.array();
@@ -118,6 +120,9 @@ public static void capture(@NonNull TestableFlutterActivity activity, @NonNull S
118120

119121
final Bitmap bitmap =
120122
InstrumentationRegistry.getInstrumentation().getUiAutomation().takeScreenshot();
123+
if (bitmap == null) {
124+
throw new RuntimeException("failed to capture screenshot");
125+
}
121126
final ByteArrayOutputStream out = new ByteArrayOutputStream();
122127
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
123128
ScreenshotUtil.writeFile(captureName, out.toByteArray());

testing/scenario_app/android/app/src/main/AndroidManifest.xml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
android:hardwareAccelerated="true"
1515
android:launchMode="singleTop"
1616
android:windowSoftInputMode="adjustResize"
17-
android:theme="@style/FullScreenScreenshot"
1817
android:exported="true">
1918
<intent-filter>
2019
<action android:name="com.google.intent.action.TEST_LOOP" />
@@ -32,7 +31,6 @@
3231
android:hardwareAccelerated="true"
3332
android:launchMode="singleTop"
3433
android:windowSoftInputMode="adjustResize"
35-
android:theme="@style/FullScreenScreenshot"
3634
android:exported="true">
3735
<intent-filter>
3836
<action android:name="android.intent.action.MAIN" />
@@ -45,7 +43,6 @@
4543
android:hardwareAccelerated="true"
4644
android:launchMode="singleTop"
4745
android:windowSoftInputMode="adjustResize"
48-
android:theme="@style/FullScreenScreenshot"
4946
android:exported="true">
5047
<intent-filter>
5148
<action android:name="android.intent.action.MAIN" />

testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TestActivity.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@
1212
import android.os.Bundle;
1313
import android.os.Handler;
1414
import android.os.Looper;
15+
import android.view.Window;
1516
import androidx.annotation.NonNull;
1617
import androidx.annotation.Nullable;
18+
import androidx.core.view.WindowCompat;
19+
import androidx.core.view.WindowInsetsCompat;
20+
import androidx.core.view.WindowInsetsControllerCompat;
1721
import io.flutter.Log;
1822
import io.flutter.embedding.engine.FlutterShellArgs;
1923
import io.flutter.embedding.engine.loader.FlutterLoader;
@@ -35,6 +39,8 @@ public abstract class TestActivity extends TestableFlutterActivity {
3539
@Override
3640
protected void onCreate(@Nullable Bundle savedInstanceState) {
3741
super.onCreate(savedInstanceState);
42+
hideSystemBars(getWindow());
43+
3844
final Intent launchIntent = getIntent();
3945
if ("com.google.intent.action.TEST_LOOP".equals(launchIntent.getAction())) {
4046
if (Build.VERSION.SDK_INT > 22) {
@@ -158,4 +164,12 @@ public void run() {
158164
}
159165
});
160166
}
167+
168+
private static void hideSystemBars(Window window) {
169+
final WindowInsetsControllerCompat insetController =
170+
WindowCompat.getInsetsController(window, window.getDecorView());
171+
insetController.setSystemBarsBehavior(
172+
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
173+
insetController.hide(WindowInsetsCompat.Type.systemBars());
174+
}
161175
}

testing/scenario_app/android/app/src/main/res/values/styles.xml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,4 @@
88
<item name="colorAccent">@color/colorAccent</item>
99
</style>
1010

11-
<style name="FullScreenScreenshot">
12-
<item name="android:windowNoTitle">true</item>
13-
<item name="android:windowActionBar">false</item>
14-
<item name="android:windowFullscreen">true</item>
15-
<item name="android:windowContentOverlay">@null</item>
16-
</style>
17-
1811
</resources>

testing/scenario_app/bin/android_integration_tests.dart

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@
33
// found in the LICENSE file.
44

55
import 'dart:async';
6-
import 'dart:convert';
76
import 'dart:io';
87
import 'dart:typed_data';
98

109
import 'package:args/args.dart';
1110
import 'package:path/path.dart';
1211
import 'package:process/process.dart';
12+
import 'package:skia_gold_client/skia_gold_client.dart';
1313

1414
import 'utils/logs.dart';
1515
import 'utils/process_manager_extension.dart';
16+
import 'utils/screenshot_transformer.dart';
1617

1718
const int tcpPort = 3001;
1819

@@ -34,9 +35,12 @@ void main(List<String> args) async {
3435
panic(<String>['cannot find adb: $adb', 'make sure to run gclient sync']);
3536
}
3637

37-
final String apkOut = join(outDir.path, 'scenario_app', 'app', 'outputs', 'apk');
38-
final File testApk = File(join(apkOut, 'androidTest', 'debug', 'app-debug-androidTest.apk'));
39-
final File appApk = File(join(apkOut, 'debug', 'app-debug.apk'));
38+
final String scenarioAppPath = join(outDir.path, 'scenario_app');
39+
final String logcatPath = join(scenarioAppPath, 'logcat.txt');
40+
final String screenshotPath = join(scenarioAppPath, 'screenshots');
41+
final String apkOutPath = join(scenarioAppPath, 'app', 'outputs', 'apk');
42+
final File testApk = File(join(apkOutPath, 'androidTest', 'debug', 'app-debug-androidTest.apk'));
43+
final File appApk = File(join(apkOutPath, 'debug', 'app-debug.apk'));
4044

4145
if (!testApk.existsSync()) {
4246
panic(<String>['test apk does not exist: ${testApk.path}', 'make sure to build the selected engine variant']);
@@ -50,30 +54,97 @@ void main(List<String> args) async {
5054
// This allows the test process to start a connection with the host, and write the bytes
5155
// for the screenshots.
5256
// On LUCI, the host uploads the screenshots to Skia Gold.
57+
SkiaGoldClient? skiaGoldClient;
5358
late ServerSocket server;
59+
final List<Future<void>> pendingComparisons = <Future<void>>[];
5460
await step('Starting server...', () async {
5561
server = await ServerSocket.bind(InternetAddress.anyIPv4, tcpPort);
5662
stdout.writeln('listening on host ${server.address.address}:${server.port}');
5763
server.listen((Socket client) {
5864
stdout.writeln('client connected ${client.remoteAddress.address}:${client.remotePort}');
59-
60-
client.listen((Uint8List data) {
61-
final int fnameLen = data.buffer.asByteData().getInt32(0);
62-
final String fileName = utf8.decode(data.buffer.asUint8List(4, fnameLen));
63-
final Uint8List fileContent = data.buffer.asUint8List(4 + fnameLen);
65+
client.transform(const ScreenshotBlobTransformer()).listen((Screenshot screenshot) {
66+
final String fileName = screenshot.filename;
67+
final Uint8List fileContent = screenshot.fileContent;
6468
log('host received ${fileContent.lengthInBytes} bytes for screenshot `$fileName`');
65-
});
69+
assert(skiaGoldClient != null, 'expected Skia Gold client');
70+
late File goldenFile;
71+
try {
72+
goldenFile = File(join(screenshotPath, fileName))..writeAsBytesSync(fileContent, flush: true);
73+
} on FileSystemException catch (err) {
74+
panic(<String>['failed to create screenshot $fileName: ${err.toString()}']);
75+
}
76+
log('wrote ${goldenFile.absolute.path}');
77+
if (isSkiaGoldClientAvailable) {
78+
final Future<void> comparison = skiaGoldClient!
79+
.addImg(fileName, goldenFile, screenshotSize: fileContent.lengthInBytes)
80+
.catchError((dynamic err) {
81+
panic(<String>['skia gold comparison failed: ${err.toString()}']);
82+
});
83+
pendingComparisons.add(comparison);
84+
}
85+
},
86+
onError: (dynamic err) {
87+
panic(<String>['error while receiving bytes: ${err.toString()}']);
88+
},
89+
cancelOnError: true);
6690
});
6791
});
6892

6993
late Process logcatProcess;
70-
final StringBuffer logcat = StringBuffer();
94+
final IOSink logcat = File(logcatPath).openWrite();
7195
try {
96+
await step('Creating screenshot directory...', () async {
97+
Directory(screenshotPath).createSync(recursive: true);
98+
});
99+
72100
await step('Starting logcat...', () async {
73-
logcatProcess = await pm.start(<String>[adb.path, 'logcat', '*:E', '-T', '1']);
101+
final int exitCode = await pm.runAndForward(<String>[adb.path, 'logcat', '-c']);
102+
if (exitCode != 0) {
103+
panic(<String>['could not clear logs']);
104+
}
105+
logcatProcess = await pm.start(<String>[adb.path, 'logcat', '-T', '1']);
74106
unawaited(pipeProcessStreams(logcatProcess, out: logcat));
75107
});
76108

109+
await step('Configuring emulator...', () async {
110+
final int exitCode = await pm.runAndForward(<String>[
111+
adb.path,
112+
'shell',
113+
'settings',
114+
'put',
115+
'secure',
116+
'immersive_mode_confirmations',
117+
'confirmed',
118+
]);
119+
if (exitCode != 0) {
120+
panic(<String>['could not configure emulator']);
121+
}
122+
});
123+
124+
await step('Get API level of connected device...', () async {
125+
final ProcessResult apiLevelProcessResult = await pm.run(<String>[adb.path, 'shell', 'getprop', 'ro.build.version.sdk']);
126+
if (apiLevelProcessResult.exitCode != 0) {
127+
panic(<String>['could not get API level of the connected device']);
128+
}
129+
final String connectedDeviceAPILevel = (apiLevelProcessResult.stdout as String).trim();
130+
log('using API level $connectedDeviceAPILevel');
131+
skiaGoldClient = SkiaGoldClient(
132+
outDir,
133+
dimensions: <String, String>{
134+
'AndroidAPILevel': connectedDeviceAPILevel,
135+
},
136+
);
137+
});
138+
139+
await step('Skia Gold auth...', () async {
140+
if (isSkiaGoldClientAvailable) {
141+
await skiaGoldClient!.auth();
142+
log('skia gold client is available');
143+
} else {
144+
log('skia gold client is unavailable');
145+
}
146+
});
147+
77148
await step('Reverse port...', () async {
78149
final int exitCode = await pm.runAndForward(<String>[adb.path, 'reverse', 'tcp:3000', 'tcp:$tcpPort']);
79150
if (exitCode != 0) {
@@ -136,11 +207,18 @@ void main(List<String> args) async {
136207
});
137208

138209
await step('Killing logcat process...', () async {
139-
logcatProcess.kill();
210+
final bool delivered = logcatProcess.kill(ProcessSignal.sigkill);
211+
assert(delivered);
140212
});
141213

142-
await step('Dumping logcat (Errors only)...', () async {
143-
stdout.write(logcat);
214+
await step('Wait for Skia gold comparisons...', () async {
215+
await Future.wait(pendingComparisons);
144216
});
217+
218+
await step('Flush logcat...', () async {
219+
await logcat.flush();
220+
});
221+
222+
exit(0);
145223
}
146224
}

testing/scenario_app/bin/utils/process_manager_extension.dart

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,46 @@ import 'dart:io';
88

99
import 'package:process/process.dart';
1010

11-
/// Pipes the [process] streams and writes them to [out].
12-
Future<int> pipeProcessStreams(Process process, {StringSink? out}) async {
11+
/// Pipes the [process] streams and writes them to [out] sink.
12+
/// If [out] is null, then the current [Process.stdout] is used as the sink.
13+
/// If [includePrefix] is true, then the prefix `[stdout]` or `[stderr]` is
14+
/// added before writting to the [out] sink.
15+
Future<int> pipeProcessStreams(
16+
Process process, {
17+
StringSink? out,
18+
bool includePrefix = true,
19+
}) async {
1320
out ??= stdout;
1421
final Completer<void> stdoutCompleter = Completer<void>();
1522
final StreamSubscription<String> stdoutSub = process.stdout
1623
.transform(utf8.decoder)
1724
.transform<String>(const LineSplitter())
1825
.listen((String line) {
19-
out!.writeln('[stdout] $line');
26+
if (includePrefix) {
27+
out!.writeln('[stdout] $line');
28+
} else {
29+
out!.writeln(line);
30+
}
2031
}, onDone: stdoutCompleter.complete);
2132

2233
final Completer<void> stderrCompleter = Completer<void>();
2334
final StreamSubscription<String> stderrSub = process.stderr
2435
.transform(utf8.decoder)
2536
.transform<String>(const LineSplitter())
2637
.listen((String line) {
27-
out!.writeln('[stderr] $line');
38+
if (includePrefix) {
39+
out!.writeln('[stderr] $line');
40+
} else {
41+
out!.writeln(line);
42+
}
2843
}, onDone: stderrCompleter.complete);
2944

3045
final int exitCode = await process.exitCode;
46+
await stderrSub.cancel();
47+
await stdoutSub.cancel();
48+
3149
await stdoutCompleter.future;
3250
await stderrCompleter.future;
33-
34-
stderrSub.cancel();
35-
stdoutSub.cancel();
3651
return exitCode;
3752
}
3853

0 commit comments

Comments
 (0)