blob: 21e97771b855dfd33fb1a12ec197b3fdb5ca9c6a [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 '../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(
// TODO(cbracken): https://github.com/flutter/flutter/issues/87508#issuecomment-1043753201
// Switch to dev/integration_tests/ui once we have CocoaPods working on M1 Macs.
'${flutterDirectory.path}/examples/hello_world',
'lib/main.dart',
release: false,
allowStderr: true,
).call;
}
TaskFunction createMacOSRunReleaseTest() {
return DesktopRunOutputTest(
// TODO(cbracken): https://github.com/flutter/flutter/issues/87508#issuecomment-1043753201
// Switch to dev/integration_tests/ui once we have CocoaPods working on M1 Macs.
'${flutterDirectory.path}/examples/hello_world',
'lib/main.dart',
release: true,
allowStderr: true,
).call;
}
TaskFunction createWindowsRunDebugTest() {
return DesktopRunOutputTest(
'${flutterDirectory.path}/dev/integration_tests/ui',
'lib/empty.dart',
release: false,
).call;
}
TaskFunction createWindowsRunReleaseTest() {
return DesktopRunOutputTest(
'${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 List<String> stderr = <String>[];
print('uninstalling...');
final Process uninstall = await startFlutter(
'install',
options: <String>['--suppress-analytics', '--uninstall-only', '-d', deviceId],
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
bool isExpectedStderr(String line) {
// TODO(egarciad): Remove once https://github.com/flutter/flutter/issues/95131 is fixed.
return line.contains('Mapping new ns');
}
@override
TaskResult verify(List<String> stdout, List<String> stderr) {
final String gradleTask = release ? 'assembleRelease' : 'assembleDebug';
final String 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);
}
}
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',
);
_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);
}
}
/// 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 Completer<void> ready = Completer<void>();
final List<String> stdout = <String>[];
final List<String> stderr = <String>[];
await prepare(deviceId);
final List<String> 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 List<String> 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 List<String> 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
''';
}
}