Add Android lifecycles test (#99319)
This commit is contained in:
parent
64d9ea60b1
commit
6c818d772a
11
.ci.yaml
11
.ci.yaml
@ -2388,6 +2388,17 @@ targets:
|
|||||||
task_name: android_choreographer_do_frame_test
|
task_name: android_choreographer_do_frame_test
|
||||||
scheduler: luci
|
scheduler: luci
|
||||||
|
|
||||||
|
- name: Linux_android android_lifecycles_test
|
||||||
|
bringup: true
|
||||||
|
recipe: devicelab/devicelab_drone
|
||||||
|
presubmit: false
|
||||||
|
timeout: 60
|
||||||
|
properties:
|
||||||
|
tags: >
|
||||||
|
["devicelab","android","linux"]
|
||||||
|
task_name: android_lifecycles_test
|
||||||
|
scheduler: luci
|
||||||
|
|
||||||
- name: Mac build_aar_module_test
|
- name: Mac build_aar_module_test
|
||||||
recipe: devicelab/devicelab_drone
|
recipe: devicelab/devicelab_drone
|
||||||
timeout: 60
|
timeout: 60
|
||||||
|
@ -10,7 +10,9 @@
|
|||||||
|
|
||||||
## Linux Android DeviceLab tests
|
## Linux Android DeviceLab tests
|
||||||
/dev/devicelab/bin/tasks/analyzer_benchmark.dart @zanderso @flutter/tool
|
/dev/devicelab/bin/tasks/analyzer_benchmark.dart @zanderso @flutter/tool
|
||||||
|
/dev/devicelab/bin/tasks/android_choreographer_do_frame_test.dart @blasten @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/android_defines_test.dart @zanderso @flutter/tool
|
/dev/devicelab/bin/tasks/android_defines_test.dart @zanderso @flutter/tool
|
||||||
|
/dev/devicelab/bin/tasks/android_lifecycles_test.dart @blasten @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/android_obfuscate_test.dart @zanderso @flutter/tool
|
/dev/devicelab/bin/tasks/android_obfuscate_test.dart @zanderso @flutter/tool
|
||||||
/dev/devicelab/bin/tasks/android_picture_cache_complexity_scoring_perf__timeline_summary.dart @flar @flutter/engine
|
/dev/devicelab/bin/tasks/android_picture_cache_complexity_scoring_perf__timeline_summary.dart @flar @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/android_stack_size_test.dart @zanderso @flutter/tool
|
/dev/devicelab/bin/tasks/android_stack_size_test.dart @zanderso @flutter/tool
|
||||||
@ -71,7 +73,6 @@
|
|||||||
/dev/devicelab/bin/tasks/opacity_peephole_fade_transition_text_perf__e2e_summary.dart @flar @flutter/engine
|
/dev/devicelab/bin/tasks/opacity_peephole_fade_transition_text_perf__e2e_summary.dart @flar @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/opacity_peephole_col_of_alpha_savelayer_rows_perf__e2e_summary.dart @flar @flutter/engine
|
/dev/devicelab/bin/tasks/opacity_peephole_col_of_alpha_savelayer_rows_perf__e2e_summary.dart @flar @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/opacity_peephole_grid_of_alpha_savelayers_perf__e2e_summary.dart @flar @flutter/engine
|
/dev/devicelab/bin/tasks/opacity_peephole_grid_of_alpha_savelayers_perf__e2e_summary.dart @flar @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/android_choreographer_do_frame_test.dart @blasten @flutter/engine
|
|
||||||
|
|
||||||
## Windows Android DeviceLab tests
|
## Windows Android DeviceLab tests
|
||||||
/dev/devicelab/bin/tasks/basic_material_app_win__compile.dart @zanderso @flutter/tool
|
/dev/devicelab/bin/tasks/basic_material_app_win__compile.dart @zanderso @flutter/tool
|
||||||
|
10
dev/devicelab/bin/tasks/android_lifecycles_test.dart
Normal file
10
dev/devicelab/bin/tasks/android_lifecycles_test.dart
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// 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 'package:flutter_devicelab/framework/framework.dart';
|
||||||
|
import 'package:flutter_devicelab/tasks/android_lifecycles_test.dart';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
await task(androidLifecyclesTest());
|
||||||
|
}
|
@ -545,7 +545,7 @@ class AndroidDevice extends Device {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Executes [command] on `adb shell` and returns its exit code.
|
/// Executes [command] on `adb shell`.
|
||||||
Future<void> shellExec(String command, List<String> arguments, { Map<String, String>? environment, bool silent = false }) async {
|
Future<void> shellExec(String command, List<String> arguments, { Map<String, String>? environment, bool silent = false }) async {
|
||||||
await adb(<String>['shell', command, ...arguments], environment: environment, silent: silent);
|
await adb(<String>['shell', command, ...arguments], environment: environment, silent: silent);
|
||||||
}
|
}
|
||||||
@ -637,7 +637,7 @@ class AndroidDevice extends Device {
|
|||||||
late final StreamController<String> stream;
|
late final StreamController<String> stream;
|
||||||
stream = StreamController<String>(
|
stream = StreamController<String>(
|
||||||
onListen: () async {
|
onListen: () async {
|
||||||
await adb(<String>['logcat', '--clear']);
|
await adb(<String>['logcat', '-c']);
|
||||||
final Process process = await startProcess(
|
final Process process = await startProcess(
|
||||||
adbPath,
|
adbPath,
|
||||||
// Make logcat less chatty by filtering down to just ActivityManager
|
// Make logcat less chatty by filtering down to just ActivityManager
|
||||||
|
@ -18,12 +18,11 @@ const List<String> kSentinelStr = <String>[
|
|||||||
'==== sentinel #3 ====',
|
'==== sentinel #3 ====',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/98973
|
/// Tests that Choreographer#doFrame finishes during application startup.
|
||||||
// This test ensures that Choreographer#doFrame finishes during application startup.
|
/// This test fails if the application hangs during this period.
|
||||||
// This test fails if the application hangs during this period.
|
/// https://ui.perfetto.dev/#!/?s=da6628c3a92456ae8fa3f345d0186e781da77e90fc8a64d073e9fee11d1e65
|
||||||
// https://ui.perfetto.dev/#!/?s=da6628c3a92456ae8fa3f345d0186e781da77e90fc8a64d073e9fee11d1e65
|
/// Regression test for https://github.com/flutter/flutter/issues/98973
|
||||||
TaskFunction androidChoreographerDoFrameTest({
|
TaskFunction androidChoreographerDoFrameTest({
|
||||||
String? deviceIdOverride,
|
|
||||||
Map<String, String>? environment,
|
Map<String, String>? environment,
|
||||||
}) {
|
}) {
|
||||||
final Directory tempDir = Directory.systemTemp
|
final Directory tempDir = Directory.systemTemp
|
||||||
@ -87,18 +86,19 @@ Future<void> main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
section('Flutter run (mode: $mode)');
|
section('Flutter run (mode: $mode)');
|
||||||
|
late Process run;
|
||||||
await inDirectory(path.join(tempDir.path, 'app'), () async {
|
await inDirectory(path.join(tempDir.path, 'app'), () async {
|
||||||
final Process run = await startProcess(
|
run = await startProcess(
|
||||||
path.join(flutterDirectory.path, 'bin', 'flutter'),
|
path.join(flutterDirectory.path, 'bin', 'flutter'),
|
||||||
flutterCommandArgs('run', <String>['--$mode', '--verbose']),
|
flutterCommandArgs('run', <String>['--$mode', '--verbose']),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
int currSentinelIdx = 0;
|
int currSentinelIdx = 0;
|
||||||
final StreamSubscription<void> stdout = run.stdout
|
final StreamSubscription<void> stdout = run.stdout
|
||||||
.transform<String>(utf8.decoder)
|
.transform<String>(utf8.decoder)
|
||||||
.transform<String>(const LineSplitter())
|
.transform<String>(const LineSplitter())
|
||||||
.listen((String line) {
|
.listen((String line) {
|
||||||
|
|
||||||
if (currSentinelIdx < sentinelCompleters.keys.length &&
|
if (currSentinelIdx < sentinelCompleters.keys.length &&
|
||||||
line.contains(sentinelCompleters.keys.elementAt(currSentinelIdx))) {
|
line.contains(sentinelCompleters.keys.elementAt(currSentinelIdx))) {
|
||||||
sentinelCompleters.values.elementAt(currSentinelIdx).complete();
|
sentinelCompleters.values.elementAt(currSentinelIdx).complete();
|
||||||
@ -107,61 +107,59 @@ Future<void> main() async {
|
|||||||
} else {
|
} else {
|
||||||
print('stdout: $line');
|
print('stdout: $line');
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final StreamSubscription<void> stderr = run.stderr
|
final StreamSubscription<void> stderr = run.stderr
|
||||||
.transform<String>(utf8.decoder)
|
.transform<String>(utf8.decoder)
|
||||||
.transform<String>(const LineSplitter())
|
.transform<String>(const LineSplitter())
|
||||||
.listen((String line) {
|
.listen((String line) {
|
||||||
print('stderr: $line');
|
print('stderr: $line');
|
||||||
});
|
});
|
||||||
|
|
||||||
final Completer<void> exitCompleter = Completer<void>();
|
final Completer<void> exitCompleter = Completer<void>();
|
||||||
|
|
||||||
unawaited(run.exitCode.then((int exitCode) {
|
unawaited(run.exitCode.then((int exitCode) {
|
||||||
exitCompleter.complete();
|
exitCompleter.complete();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
section('Wait for sentinels (mode: $mode)');
|
section('Wait for sentinels (mode: $mode)');
|
||||||
for (final Completer<void> completer in sentinelCompleters.values) {
|
for (final Completer<void> completer in sentinelCompleters.values) {
|
||||||
if (nextCompleterIdx == 0) {
|
if (nextCompleterIdx == 0) {
|
||||||
// Don't time out because we don't know how long it would take to get the first log.
|
// Don't time out because we don't know how long it would take to get the first log.
|
||||||
|
await Future.any<dynamic>(
|
||||||
|
<Future<dynamic>>[
|
||||||
|
completer.future,
|
||||||
|
exitCompleter.future,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// Time out since this should not take 1s after the first log was received.
|
||||||
await Future.any<dynamic>(
|
await Future.any<dynamic>(
|
||||||
<Future<dynamic>>[
|
<Future<dynamic>>[
|
||||||
completer.future,
|
completer.future.timeout(const Duration(seconds: 1)),
|
||||||
exitCompleter.future,
|
exitCompleter.future,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} on TimeoutException {
|
||||||
try {
|
|
||||||
// Time out since this should not take 1s after the first log was received.
|
|
||||||
await Future.any<dynamic>(
|
|
||||||
<Future<dynamic>>[
|
|
||||||
completer.future.timeout(const Duration(seconds: 1)),
|
|
||||||
exitCompleter.future,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} on TimeoutException {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (exitCompleter.isCompleted) {
|
|
||||||
// The process exited.
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
nextCompleterIdx++;
|
|
||||||
}
|
}
|
||||||
|
if (exitCompleter.isCompleted) {
|
||||||
|
// The process exited.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
nextCompleterIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
section('Quit app (mode: $mode)');
|
section('Quit app (mode: $mode)');
|
||||||
run.stdin.write('q');
|
run.stdin.write('q');
|
||||||
await exitCompleter.future;
|
await exitCompleter.future;
|
||||||
|
|
||||||
section('Stop listening to stdout and stderr (mode: $mode)');
|
section('Stop listening to stdout and stderr (mode: $mode)');
|
||||||
await stdout.cancel();
|
await stdout.cancel();
|
||||||
await stderr.cancel();
|
await stderr.cancel();
|
||||||
run.kill();
|
run.kill();
|
||||||
});
|
|
||||||
|
|
||||||
if (nextCompleterIdx == sentinelCompleters.values.length) {
|
if (nextCompleterIdx == sentinelCompleters.values.length) {
|
||||||
return TaskResult.success(null);
|
return TaskResult.success(null);
|
||||||
|
212
dev/devicelab/lib/tasks/android_lifecycles_test.dart
Normal file
212
dev/devicelab/lib/tasks/android_lifecycles_test.dart
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
// 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 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import '../framework/devices.dart';
|
||||||
|
import '../framework/framework.dart';
|
||||||
|
import '../framework/task_result.dart';
|
||||||
|
import '../framework/utils.dart';
|
||||||
|
|
||||||
|
const String _kOrgName = 'com.example.activitydestroy';
|
||||||
|
|
||||||
|
final RegExp _lifecycleSentinelRegExp = RegExp(r'==== lifecycle\: (.+) ====');
|
||||||
|
|
||||||
|
/// Tests the following Android lifecycles: Activity#onStop(), Activity#onResume(), Activity#onPause()
|
||||||
|
/// from Dart perspective in debug, profile, and release modes.
|
||||||
|
TaskFunction androidLifecyclesTest({
|
||||||
|
Map<String, String>? environment,
|
||||||
|
}) {
|
||||||
|
final Directory tempDir = Directory.systemTemp
|
||||||
|
.createTempSync('flutter_devicelab_activity_destroy.');
|
||||||
|
return () async {
|
||||||
|
try {
|
||||||
|
section('Create app');
|
||||||
|
await inDirectory(tempDir, () async {
|
||||||
|
await flutter(
|
||||||
|
'create',
|
||||||
|
options: <String>[
|
||||||
|
'--platforms',
|
||||||
|
'android',
|
||||||
|
'--org',
|
||||||
|
_kOrgName,
|
||||||
|
'app',
|
||||||
|
],
|
||||||
|
environment: environment,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final File mainDart = File(path.join(
|
||||||
|
tempDir.absolute.path,
|
||||||
|
'app',
|
||||||
|
'lib',
|
||||||
|
'main.dart',
|
||||||
|
));
|
||||||
|
if (!mainDart.existsSync()) {
|
||||||
|
return TaskResult.failure('${mainDart.path} does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
section('Patch lib/main.dart');
|
||||||
|
await mainDart.writeAsString(r'''
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class LifecycleObserver extends WidgetsBindingObserver {
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
print('==== lifecycle: $state ====');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
WidgetsBinding.instance.addObserver(LifecycleObserver());
|
||||||
|
runApp(Container());
|
||||||
|
}
|
||||||
|
''', flush: true);
|
||||||
|
|
||||||
|
final List<String> androidLifecycles = <String>[];
|
||||||
|
|
||||||
|
Future<TaskResult> runTestFor(String mode) async {
|
||||||
|
section('Flutter run (mode: $mode)');
|
||||||
|
|
||||||
|
late Process run;
|
||||||
|
await inDirectory(path.join(tempDir.path, 'app'), () async {
|
||||||
|
run = await startProcess(
|
||||||
|
path.join(flutterDirectory.path, 'bin', 'flutter'),
|
||||||
|
flutterCommandArgs('run', <String>['--$mode']),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final AndroidDevice device = await devices.workingDevice as AndroidDevice;
|
||||||
|
await device.unlock();
|
||||||
|
|
||||||
|
final StreamController<String> lifecyles = StreamController<String>();
|
||||||
|
|
||||||
|
final StreamSubscription<void> stdout = run.stdout
|
||||||
|
.transform<String>(utf8.decoder)
|
||||||
|
.transform<String>(const LineSplitter())
|
||||||
|
.listen((String log) {
|
||||||
|
final RegExpMatch? match = _lifecycleSentinelRegExp.firstMatch(log);
|
||||||
|
print('stdout: $log');
|
||||||
|
if (match == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String lifecycle = match[1]!;
|
||||||
|
androidLifecycles.add(lifecycle);
|
||||||
|
|
||||||
|
print('stdout: Found app lifecycle: $lifecycle');
|
||||||
|
lifecyles.add(lifecycle);
|
||||||
|
});
|
||||||
|
|
||||||
|
final StreamSubscription<void> stderr = run.stderr
|
||||||
|
.transform<String>(utf8.decoder)
|
||||||
|
.transform<String>(const LineSplitter())
|
||||||
|
.listen((String log) {
|
||||||
|
print('stderr: $log');
|
||||||
|
});
|
||||||
|
|
||||||
|
final StreamIterator<String> lifecycleItr = StreamIterator<String>(lifecyles.stream);
|
||||||
|
|
||||||
|
{
|
||||||
|
const String expected = 'AppLifecycleState.resumed';
|
||||||
|
await lifecycleItr.moveNext();
|
||||||
|
final String got = lifecycleItr.current;
|
||||||
|
if (expected != got) {
|
||||||
|
return TaskResult.failure('expected lifecycles: `$expected`, but got` $got`');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section('Toggling app switch (mode: $mode)');
|
||||||
|
await device.shellExec('input', <String>['keyevent', 'KEYCODE_APP_SWITCH']);
|
||||||
|
|
||||||
|
{
|
||||||
|
const String expected = 'AppLifecycleState.inactive';
|
||||||
|
await lifecycleItr.moveNext();
|
||||||
|
final String got = lifecycleItr.current;
|
||||||
|
if (expected != got) {
|
||||||
|
return TaskResult.failure('expected lifecycles: `$expected`, but got` $got`');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section('Bring activity to foreground (mode: $mode)');
|
||||||
|
await device.shellExec('am', <String>['start', '--activity-single-top', '$_kOrgName.app/.MainActivity']);
|
||||||
|
|
||||||
|
{
|
||||||
|
const String expected = 'AppLifecycleState.resumed';
|
||||||
|
await lifecycleItr.moveNext();
|
||||||
|
final String got = lifecycleItr.current;
|
||||||
|
if (expected != got) {
|
||||||
|
return TaskResult.failure('expected lifecycles: `$expected`, but got` $got`');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section('Launch Settings app (mode: $mode)');
|
||||||
|
await device.shellExec('am', <String>['start', '-a', 'android.settings.SETTINGS']);
|
||||||
|
|
||||||
|
{
|
||||||
|
const String expected = 'AppLifecycleState.inactive';
|
||||||
|
await lifecycleItr.moveNext();
|
||||||
|
final String got = lifecycleItr.current;
|
||||||
|
if (expected != got) {
|
||||||
|
return TaskResult.failure('expected lifecycles: `$expected`, but got` $got`');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const String expected = 'AppLifecycleState.paused';
|
||||||
|
await lifecycleItr.moveNext();
|
||||||
|
final String got = lifecycleItr.current;
|
||||||
|
if (expected != got) {
|
||||||
|
return TaskResult.failure('expected lifecycles: `$expected`, but got` $got`');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section('Bring activity to foreground (mode: $mode)');
|
||||||
|
await device.shellExec('am', <String>['start', '--activity-single-top', '$_kOrgName.app/.MainActivity']);
|
||||||
|
|
||||||
|
{
|
||||||
|
const String expected = 'AppLifecycleState.resumed';
|
||||||
|
await lifecycleItr.moveNext();
|
||||||
|
final String got = lifecycleItr.current;
|
||||||
|
if (expected != got) {
|
||||||
|
return TaskResult.failure('expected lifecycles: `$expected`, but got` $got`');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run.kill();
|
||||||
|
|
||||||
|
section('Stop subscriptions (mode: $mode)');
|
||||||
|
|
||||||
|
await lifecycleItr.cancel();
|
||||||
|
await lifecyles.close();
|
||||||
|
await stdout.cancel();
|
||||||
|
await stderr.cancel();
|
||||||
|
return TaskResult.success(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final TaskResult debugResult = await runTestFor('debug');
|
||||||
|
if (debugResult.failed) {
|
||||||
|
return debugResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
final TaskResult profileResult = await runTestFor('profile');
|
||||||
|
if (profileResult.failed) {
|
||||||
|
return profileResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
final TaskResult releaseResult = await runTestFor('release');
|
||||||
|
if (releaseResult.failed) {
|
||||||
|
return releaseResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TaskResult.success(null);
|
||||||
|
} finally {
|
||||||
|
rmTree(tempDir);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user