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