| // 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); |
| } |
| }; |
| } |