blob: d55b11e29229e876ef4aee714475c96bc6a96e4b [file] [log] [blame]
// 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/framework.dart';
import '../framework/task_result.dart';
import '../framework/utils.dart';
const List<String> kSentinelStr = <String>[
'==== sentinel #1 ====',
'==== sentinel #2 ====',
'==== sentinel #3 ====',
];
/// Tests that Choreographer#doFrame finishes during application startup.
/// This test fails if the application hangs during this period.
/// https://ui.perfetto.dev/#!/?s=da6628c3a92456ae8fa3f345d0186e781da77e90fc8a64d073e9fee11d1e65
/// Regression test for https://github.com/flutter/flutter/issues/98973
TaskFunction androidChoreographerDoFrameTest({
Map<String, String>? environment,
}) {
final Directory tempDir = Directory.systemTemp
.createTempSync('flutter_devicelab_android_surface_recreation.');
return () async {
try {
section('Create app');
await inDirectory(tempDir, () async {
await flutter(
'create',
options: <String>[
'--platforms',
'android',
'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('''
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
print('${kSentinelStr[0]}');
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
print('${kSentinelStr[1]}');
// If the Android UI thread is blocked, then this Future won't resolve.
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
print('${kSentinelStr[2]}');
runApp(
Container(
decoration: BoxDecoration(
color: const Color(0xff7c94b6),
),
),
);
}
''', flush: true);
Future<TaskResult> runTestFor(String mode) async {
int nextCompleterIdx = 0;
final Map<String, Completer<void>> sentinelCompleters = <String, Completer<void>>{};
for (final String sentinel in kSentinelStr) {
sentinelCompleters[sentinel] = Completer<void>();
}
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', '--verbose']),
);
});
int currSentinelIdx = 0;
final StreamSubscription<void> stdout = run.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
if (currSentinelIdx < sentinelCompleters.keys.length &&
line.contains(sentinelCompleters.keys.elementAt(currSentinelIdx))) {
sentinelCompleters.values.elementAt(currSentinelIdx).complete();
currSentinelIdx++;
print('stdout(MATCHED): $line');
} else {
print('stdout: $line');
}
});
final StreamSubscription<void> stderr = run.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('stderr: $line');
});
final Completer<void> exitCompleter = Completer<void>();
unawaited(run.exitCode.then((int exitCode) {
exitCompleter.complete();
}));
section('Wait for sentinels (mode: $mode)');
for (final Completer<void> completer in sentinelCompleters.values) {
if (nextCompleterIdx == 0) {
// 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>(
<Future<dynamic>>[
completer.future.timeout(const Duration(seconds: 1)),
exitCompleter.future,
],
);
} on TimeoutException {
break;
}
}
if (exitCompleter.isCompleted) {
// The process exited.
break;
}
nextCompleterIdx++;
}
section('Quit app (mode: $mode)');
run.stdin.write('q');
await exitCompleter.future;
section('Stop listening to stdout and stderr (mode: $mode)');
await stdout.cancel();
await stderr.cancel();
run.kill();
if (nextCompleterIdx == sentinelCompleters.values.length) {
return TaskResult.success(null);
}
final String nextSentinel = sentinelCompleters.keys.elementAt(nextCompleterIdx);
return TaskResult.failure('Expected sentinel `$nextSentinel` in mode $mode');
}
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);
}
};
}