| // 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:ffi'; | |
| import 'dart:io'; | |
| import '../framework/devices.dart'; | |
| import '../framework/framework.dart'; | |
| import '../framework/task_result.dart'; | |
| import '../framework/utils.dart'; | |
| TaskFunction createAndroidRunDebugTest() { | |
| return AndroidRunOutputTest(release: false).call; | |
| } | |
| TaskFunction createAndroidRunReleaseTest() { | |
| return AndroidRunOutputTest(release: true).call; | |
| } | |
| TaskFunction createLinuxRunDebugTest() { | |
| return DesktopRunOutputTest( | |
| '${flutterDirectory.path}/dev/integration_tests/ui', | |
| 'lib/empty.dart', | |
| release: false, | |
| ).call; | |
| } | |
| TaskFunction createLinuxRunReleaseTest() { | |
| return DesktopRunOutputTest( | |
| '${flutterDirectory.path}/dev/integration_tests/ui', | |
| 'lib/empty.dart', | |
| release: true, | |
| ).call; | |
| } | |
| TaskFunction createMacOSRunDebugTest() { | |
| return DesktopRunOutputTest( | |
| '${flutterDirectory.path}/dev/integration_tests/ui', | |
| 'lib/main.dart', | |
| release: false, | |
| allowStderr: true, | |
| ).call; | |
| } | |
| TaskFunction createMacOSRunReleaseTest() { | |
| return DesktopRunOutputTest( | |
| '${flutterDirectory.path}/dev/integration_tests/ui', | |
| 'lib/main.dart', | |
| release: true, | |
| allowStderr: true, | |
| ).call; | |
| } | |
| TaskFunction createWindowsRunDebugTest() { | |
| return WindowsRunOutputTest( | |
| '${flutterDirectory.path}/dev/integration_tests/ui', | |
| 'lib/empty.dart', | |
| release: false, | |
| ).call; | |
| } | |
| TaskFunction createWindowsRunReleaseTest() { | |
| return WindowsRunOutputTest( | |
| '${flutterDirectory.path}/dev/integration_tests/ui', | |
| 'lib/empty.dart', | |
| release: true, | |
| ).call; | |
| } | |
| class AndroidRunOutputTest extends RunOutputTask { | |
| AndroidRunOutputTest({required super.release}) | |
| : super('${flutterDirectory.path}/dev/integration_tests/ui', 'lib/main.dart'); | |
| @override | |
| Future<void> prepare(String deviceId) async { | |
| // Uninstall if the app is already installed on the device to get to a clean state. | |
| final stderr = <String>[]; | |
| print('uninstalling...'); | |
| final Process uninstall = await startFlutter( | |
| 'install', | |
| // TODO(andrewkolos): consider removing -v after | |
| // https://github.com/flutter/flutter/issues/153367 is troubleshot. | |
| options: <String>['--suppress-analytics', '--uninstall-only', '-d', deviceId, '-v'], | |
| isBot: false, | |
| ); | |
| uninstall.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen( | |
| (String line) { | |
| print('uninstall:stdout: $line'); | |
| }, | |
| ); | |
| uninstall.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen( | |
| (String line) { | |
| print('uninstall:stderr: $line'); | |
| stderr.add(line); | |
| }, | |
| ); | |
| if (await uninstall.exitCode != 0) { | |
| throw 'flutter install --uninstall-only failed.'; | |
| } | |
| if (stderr.isNotEmpty) { | |
| throw 'flutter install --uninstall-only had output on standard error.'; | |
| } | |
| } | |
| @override | |
| TaskResult verify(List<String> stdout, List<String> stderr) { | |
| final gradleTask = release ? 'assembleRelease' : 'assembleDebug'; | |
| final apk = release ? 'app-release.apk' : 'app-debug.apk'; | |
| _findNextMatcherInList( | |
| stdout, | |
| (String line) => | |
| line.startsWith('Launching lib/main.dart on ') && | |
| line.endsWith(' in ${release ? 'release' : 'debug'} mode...'), | |
| 'Launching lib/main.dart on', | |
| ); | |
| _findNextMatcherInList( | |
| stdout, | |
| (String line) => line.startsWith("Running Gradle task '$gradleTask'..."), | |
| "Running Gradle task '$gradleTask'...", | |
| ); | |
| // Size information is only included in release builds. | |
| _findNextMatcherInList( | |
| stdout, | |
| (String line) => | |
| line.contains('Built build/app/outputs/flutter-apk/$apk') && | |
| (!release || line.contains('MB)')), | |
| 'Built build/app/outputs/flutter-apk/$apk', | |
| ); | |
| _findNextMatcherInList( | |
| stdout, | |
| (String line) => line.startsWith('Installing build/app/outputs/flutter-apk/$apk...'), | |
| 'Installing build/app/outputs/flutter-apk/$apk...', | |
| ); | |
| _findNextMatcherInList( | |
| stdout, | |
| (String line) => line.contains('Quit (terminate the application on the device).'), | |
| 'q Quit (terminate the application on the device)', | |
| ); | |
| _findNextMatcherInList( | |
| stdout, | |
| (String line) => line == 'Application finished.', | |
| 'Application finished.', | |
| ); | |
| return TaskResult.success(null); | |
| } | |
| @override | |
| bool isExpectedStderr(String line) { | |
| // Gradle sometimes "terminates" unexpectedly when starting the daemon. This isn't actually an | |
| // error and the build completes successfully. | |
| // | |
| // See https://github.com/flutter/flutter/issues/177371 | |
| final exactMatches = <String>{ | |
| 'e: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:', | |
| '1. Kotlin compile daemon is ready', | |
| }; | |
| if (exactMatches.contains(line.trim())) { | |
| return true; | |
| } | |
| // Checks multi-line migrate to built-in Kotlin warning. This is not an | |
| // actual error and the build completes successfully. | |
| // | |
| // See https://github.com/flutter/flutter/issues/183909 | |
| return line.contains('Your Android app project: app located at:') || | |
| line.contains('applies the Kotlin Gradle Plugin') || | |
| line.contains('Please migrate your app to Built-in Kotlin') || | |
| line.trim().isEmpty; | |
| } | |
| } | |
| class WindowsRunOutputTest extends DesktopRunOutputTest { | |
| WindowsRunOutputTest( | |
| super.testDirectory, | |
| super.testTarget, { | |
| required super.release, | |
| super.allowStderr = false, | |
| }); | |
| final String arch = Abi.current() == Abi.windowsX64 ? 'x64' : 'arm64'; | |
| static final RegExp _buildOutput = RegExp( | |
| r'Building Windows application\.\.\.\s*\d+(\.\d+)?(ms|s)', | |
| multiLine: true, | |
| ); | |
| static final RegExp _builtOutput = RegExp( | |
| r'Built build\\windows\\(x64|arm64)\\runner\\(Debug|Release)\\\w+\.exe( \(\d+(\.\d+)?MB\))?', | |
| ); | |
| @override | |
| void verifyBuildOutput(List<String> stdout) { | |
| _findNextMatcherInList(stdout, _buildOutput.hasMatch, 'Building Windows application...'); | |
| final buildMode = release ? 'Release' : 'Debug'; | |
| _findNextMatcherInList(stdout, (String line) { | |
| if (!_builtOutput.hasMatch(line) || !line.contains(buildMode)) { | |
| return false; | |
| } | |
| return true; | |
| }, '√ Built build\\windows\\$arch\\runner\\$buildMode\\ui.exe'); | |
| } | |
| } | |
| class DesktopRunOutputTest extends RunOutputTask { | |
| DesktopRunOutputTest( | |
| super.testDirectory, | |
| super.testTarget, { | |
| required super.release, | |
| this.allowStderr = false, | |
| }); | |
| /// Whether `flutter run` is expected to produce output on stderr. | |
| final bool allowStderr; | |
| @override | |
| bool isExpectedStderr(String line) => allowStderr; | |
| @override | |
| TaskResult verify(List<String> stdout, List<String> stderr) { | |
| _findNextMatcherInList( | |
| stdout, | |
| (String line) => | |
| line.startsWith('Launching $testTarget on ') && | |
| line.endsWith(' in ${release ? 'release' : 'debug'} mode...'), | |
| 'Launching $testTarget on', | |
| ); | |
| verifyBuildOutput(stdout); | |
| _findNextMatcherInList( | |
| stdout, | |
| (String line) => line.contains('Quit (terminate the application on the device).'), | |
| 'q Quit (terminate the application on the device)', | |
| ); | |
| _findNextMatcherInList( | |
| stdout, | |
| (String line) => line == 'Application finished.', | |
| 'Application finished.', | |
| ); | |
| return TaskResult.success(null); | |
| } | |
| /// Verify the output from `flutter run`'s build step. | |
| void verifyBuildOutput(List<String> stdout) {} | |
| } | |
| /// Test that the output of `flutter run` is expected. | |
| abstract class RunOutputTask { | |
| RunOutputTask(this.testDirectory, this.testTarget, {required this.release}); | |
| static final RegExp _engineLogRegex = RegExp(r'\[(VERBOSE|INFO|WARNING|ERROR|FATAL):.+\(\d+\)\]'); | |
| /// The directory where the app under test is defined. | |
| final String testDirectory; | |
| /// The main entry-point file of the application, as run on the device. | |
| final String testTarget; | |
| /// Whether to run the app in release mode. | |
| final bool release; | |
| Future<TaskResult> call() { | |
| return inDirectory<TaskResult>(testDirectory, () async { | |
| final Device device = await devices.workingDevice; | |
| await device.unlock(); | |
| final String deviceId = device.deviceId; | |
| final ready = Completer<void>(); | |
| final stdout = <String>[]; | |
| final stderr = <String>[]; | |
| await prepare(deviceId); | |
| final options = <String>[testTarget, '-d', deviceId, if (release) '--release']; | |
| final Process run = await startFlutter('run', options: options, isBot: false); | |
| int? runExitCode; | |
| run.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(( | |
| String line, | |
| ) { | |
| print('run:stdout: $line'); | |
| stdout.add(line); | |
| if (line.contains('Quit (terminate the application on the device).')) { | |
| ready.complete(); | |
| } | |
| }); | |
| final Stream<String> runStderr = run.stderr | |
| .transform<String>(utf8.decoder) | |
| .transform<String>(const LineSplitter()) | |
| .asBroadcastStream(); | |
| runStderr.listen((String line) => print('run:stderr: $line')); | |
| runStderr.skipWhile(isExpectedStderr).listen((String line) => stderr.add(line)); | |
| unawaited( | |
| run.exitCode.then<void>((int exitCode) { | |
| runExitCode = exitCode; | |
| }), | |
| ); | |
| await Future.any<dynamic>(<Future<dynamic>>[ready.future, run.exitCode]); | |
| if (runExitCode != null) { | |
| throw 'Failed to run test app; runner unexpected exited, with exit code $runExitCode.'; | |
| } | |
| run.stdin.write('q'); | |
| await run.exitCode; | |
| if (stderr.isNotEmpty) { | |
| throw 'flutter run ${release ? '--release' : ''} had unexpected output on standard error.'; | |
| } | |
| final engineLogs = List<String>.from(stdout.where(_engineLogRegex.hasMatch)); | |
| if (engineLogs.isNotEmpty) { | |
| throw 'flutter run had unexpected Flutter engine logs $engineLogs'; | |
| } | |
| return verify(stdout, stderr); | |
| }); | |
| } | |
| /// Prepare the device for running the test app. | |
| Future<void> prepare(String deviceId) => Future<void>.value(); | |
| /// Returns true if this stderr output line is expected. | |
| bool isExpectedStderr(String line) => false; | |
| /// Verify the output of `flutter run`. | |
| TaskResult verify(List<String> stdout, List<String> stderr) => | |
| throw UnimplementedError('verify is not implemented'); | |
| /// Helper that verifies a line in [list] matches [matcher]. | |
| /// The [list] is updated to contain the lines remaining after the match. | |
| void _findNextMatcherInList( | |
| List<String> list, | |
| bool Function(String testLine) matcher, | |
| String errorMessageExpectedLine, | |
| ) { | |
| final copyOfListForErrorMessage = List<String>.from(list); | |
| while (list.isNotEmpty) { | |
| final String nextLine = list.first; | |
| list.removeAt(0); | |
| if (matcher(nextLine)) { | |
| return; | |
| } | |
| } | |
| throw ''' | |
| Did not find expected line | |
| $errorMessageExpectedLine | |
| in flutter run ${release ? '--release' : ''} stdout | |
| $copyOfListForErrorMessage | |
| '''; | |
| } | |
| } |