1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:convert';
7import 'dart:ffi';
8import 'dart:io';
9
10import '../framework/devices.dart';
11import '../framework/framework.dart';
12import '../framework/task_result.dart';
13import '../framework/utils.dart';
14
15TaskFunction createAndroidRunDebugTest() {
16 return AndroidRunOutputTest(release: false).call;
17}
18
19TaskFunction createAndroidRunReleaseTest() {
20 return AndroidRunOutputTest(release: true).call;
21}
22
23TaskFunction createLinuxRunDebugTest() {
24 return DesktopRunOutputTest(
25 '${flutterDirectory.path}/dev/integration_tests/ui',
26 'lib/empty.dart',
27 release: false,
28 ).call;
29}
30
31TaskFunction createLinuxRunReleaseTest() {
32 return DesktopRunOutputTest(
33 '${flutterDirectory.path}/dev/integration_tests/ui',
34 'lib/empty.dart',
35 release: true,
36 ).call;
37}
38
39TaskFunction createMacOSRunDebugTest() {
40 return DesktopRunOutputTest(
41 '${flutterDirectory.path}/dev/integration_tests/ui',
42 'lib/main.dart',
43 release: false,
44 allowStderr: true,
45 ).call;
46}
47
48TaskFunction createMacOSRunReleaseTest() {
49 return DesktopRunOutputTest(
50 '${flutterDirectory.path}/dev/integration_tests/ui',
51 'lib/main.dart',
52 release: true,
53 allowStderr: true,
54 ).call;
55}
56
57TaskFunction createWindowsRunDebugTest() {
58 return WindowsRunOutputTest(
59 '${flutterDirectory.path}/dev/integration_tests/ui',
60 'lib/empty.dart',
61 release: false,
62 ).call;
63}
64
65TaskFunction createWindowsRunReleaseTest() {
66 return WindowsRunOutputTest(
67 '${flutterDirectory.path}/dev/integration_tests/ui',
68 'lib/empty.dart',
69 release: true,
70 ).call;
71}
72
73class AndroidRunOutputTest extends RunOutputTask {
74 AndroidRunOutputTest({required super.release})
75 : super('${flutterDirectory.path}/dev/integration_tests/ui', 'lib/main.dart');
76
77 @override
78 Future<void> prepare(String deviceId) async {
79 // Uninstall if the app is already installed on the device to get to a clean state.
80 final List<String> stderr = <String>[];
81 print('uninstalling...');
82 final Process uninstall = await startFlutter(
83 'install',
84 // TODO(andrewkolos): consider removing -v after
85 // https://github.com/flutter/flutter/issues/153367 is troubleshot.
86 options: <String>['--suppress-analytics', '--uninstall-only', '-d', deviceId, '-v'],
87 isBot: false,
88 );
89 uninstall.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(
90 (String line) {
91 print('uninstall:stdout: $line');
92 },
93 );
94 uninstall.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(
95 (String line) {
96 print('uninstall:stderr: $line');
97 stderr.add(line);
98 },
99 );
100 if (await uninstall.exitCode != 0) {
101 throw 'flutter install --uninstall-only failed.';
102 }
103 if (stderr.isNotEmpty) {
104 throw 'flutter install --uninstall-only had output on standard error.';
105 }
106 }
107
108 @override
109 TaskResult verify(List<String> stdout, List<String> stderr) {
110 final String gradleTask = release ? 'assembleRelease' : 'assembleDebug';
111 final String apk = release ? 'app-release.apk' : 'app-debug.apk';
112
113 _findNextMatcherInList(
114 stdout,
115 (String line) =>
116 line.startsWith('Launching lib/main.dart on ') &&
117 line.endsWith(' in ${release ? 'release' : 'debug'} mode...'),
118 'Launching lib/main.dart on',
119 );
120
121 _findNextMatcherInList(
122 stdout,
123 (String line) => line.startsWith("Running Gradle task '$gradleTask'..."),
124 "Running Gradle task '$gradleTask'...",
125 );
126
127 // Size information is only included in release builds.
128 _findNextMatcherInList(
129 stdout,
130 (String line) =>
131 line.contains('Built build/app/outputs/flutter-apk/$apk') &&
132 (!release || line.contains('MB)')),
133 'Built build/app/outputs/flutter-apk/$apk',
134 );
135
136 _findNextMatcherInList(
137 stdout,
138 (String line) => line.startsWith('Installing build/app/outputs/flutter-apk/$apk...'),
139 'Installing build/app/outputs/flutter-apk/$apk...',
140 );
141
142 _findNextMatcherInList(
143 stdout,
144 (String line) => line.contains('Quit (terminate the application on the device).'),
145 'q Quit (terminate the application on the device)',
146 );
147
148 _findNextMatcherInList(
149 stdout,
150 (String line) => line == 'Application finished.',
151 'Application finished.',
152 );
153
154 return TaskResult.success(null);
155 }
156}
157
158class WindowsRunOutputTest extends DesktopRunOutputTest {
159 WindowsRunOutputTest(
160 super.testDirectory,
161 super.testTarget, {
162 required super.release,
163 super.allowStderr = false,
164 });
165
166 final String arch = Abi.current() == Abi.windowsX64 ? 'x64' : 'arm64';
167
168 static final RegExp _buildOutput = RegExp(
169 r'Building Windows application\.\.\.\s*\d+(\.\d+)?(ms|s)',
170 multiLine: true,
171 );
172 static final RegExp _builtOutput = RegExp(
173 r'Built build\\windows\\(x64|arm64)\\runner\\(Debug|Release)\\\w+\.exe( \(\d+(\.\d+)?MB\))?',
174 );
175
176 @override
177 void verifyBuildOutput(List<String> stdout) {
178 _findNextMatcherInList(stdout, _buildOutput.hasMatch, 'Building Windows application...');
179
180 final String buildMode = release ? 'Release' : 'Debug';
181 _findNextMatcherInList(stdout, (String line) {
182 if (!_builtOutput.hasMatch(line) || !line.contains(buildMode)) {
183 return false;
184 }
185
186 return true;
187 }, '√ Built build\\windows\\$arch\\runner\\$buildMode\\ui.exe');
188 }
189}
190
191class DesktopRunOutputTest extends RunOutputTask {
192 DesktopRunOutputTest(
193 super.testDirectory,
194 super.testTarget, {
195 required super.release,
196 this.allowStderr = false,
197 });
198
199 /// Whether `flutter run` is expected to produce output on stderr.
200 final bool allowStderr;
201
202 @override
203 bool isExpectedStderr(String line) => allowStderr;
204
205 @override
206 TaskResult verify(List<String> stdout, List<String> stderr) {
207 _findNextMatcherInList(
208 stdout,
209 (String line) =>
210 line.startsWith('Launching $testTarget on ') &&
211 line.endsWith(' in ${release ? 'release' : 'debug'} mode...'),
212 'Launching $testTarget on',
213 );
214
215 verifyBuildOutput(stdout);
216
217 _findNextMatcherInList(
218 stdout,
219 (String line) => line.contains('Quit (terminate the application on the device).'),
220 'q Quit (terminate the application on the device)',
221 );
222
223 _findNextMatcherInList(
224 stdout,
225 (String line) => line == 'Application finished.',
226 'Application finished.',
227 );
228
229 return TaskResult.success(null);
230 }
231
232 /// Verify the output from `flutter run`'s build step.
233 void verifyBuildOutput(List<String> stdout) {}
234}
235
236/// Test that the output of `flutter run` is expected.
237abstract class RunOutputTask {
238 RunOutputTask(this.testDirectory, this.testTarget, {required this.release});
239
240 static final RegExp _engineLogRegex = RegExp(r'\[(VERBOSE|INFO|WARNING|ERROR|FATAL):.+\(\d+\)\]');
241
242 /// The directory where the app under test is defined.
243 final String testDirectory;
244
245 /// The main entry-point file of the application, as run on the device.
246 final String testTarget;
247
248 /// Whether to run the app in release mode.
249 final bool release;
250
251 Future<TaskResult> call() {
252 return inDirectory<TaskResult>(testDirectory, () async {
253 final Device device = await devices.workingDevice;
254 await device.unlock();
255 final String deviceId = device.deviceId;
256
257 final Completer<void> ready = Completer<void>();
258 final List<String> stdout = <String>[];
259 final List<String> stderr = <String>[];
260
261 await prepare(deviceId);
262
263 final List<String> options = <String>[testTarget, '-d', deviceId, if (release) '--release'];
264
265 final Process run = await startFlutter('run', options: options, isBot: false);
266
267 int? runExitCode;
268 run.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen((
269 String line,
270 ) {
271 print('run:stdout: $line');
272 stdout.add(line);
273 if (line.contains('Quit (terminate the application on the device).')) {
274 ready.complete();
275 }
276 });
277 final Stream<String> runStderr = run.stderr
278 .transform<String>(utf8.decoder)
279 .transform<String>(const LineSplitter())
280 .asBroadcastStream();
281 runStderr.listen((String line) => print('run:stderr: $line'));
282 runStderr.skipWhile(isExpectedStderr).listen((String line) => stderr.add(line));
283 unawaited(
284 run.exitCode.then<void>((int exitCode) {
285 runExitCode = exitCode;
286 }),
287 );
288 await Future.any<dynamic>(<Future<dynamic>>[ready.future, run.exitCode]);
289 if (runExitCode != null) {
290 throw 'Failed to run test app; runner unexpected exited, with exit code $runExitCode.';
291 }
292 run.stdin.write('q');
293
294 await run.exitCode;
295
296 if (stderr.isNotEmpty) {
297 throw 'flutter run ${release ? '--release' : ''} had unexpected output on standard error.';
298 }
299
300 final List<String> engineLogs = List<String>.from(stdout.where(_engineLogRegex.hasMatch));
301 if (engineLogs.isNotEmpty) {
302 throw 'flutter run had unexpected Flutter engine logs $engineLogs';
303 }
304
305 return verify(stdout, stderr);
306 });
307 }
308
309 /// Prepare the device for running the test app.
310 Future<void> prepare(String deviceId) => Future<void>.value();
311
312 /// Returns true if this stderr output line is expected.
313 bool isExpectedStderr(String line) => false;
314
315 /// Verify the output of `flutter run`.
316 TaskResult verify(List<String> stdout, List<String> stderr) =>
317 throw UnimplementedError('verify is not implemented');
318
319 /// Helper that verifies a line in [list] matches [matcher].
320 /// The [list] is updated to contain the lines remaining after the match.
321 void _findNextMatcherInList(
322 List<String> list,
323 bool Function(String testLine) matcher,
324 String errorMessageExpectedLine,
325 ) {
326 final List<String> copyOfListForErrorMessage = List<String>.from(list);
327
328 while (list.isNotEmpty) {
329 final String nextLine = list.first;
330 list.removeAt(0);
331
332 if (matcher(nextLine)) {
333 return;
334 }
335 }
336
337 throw '''
338Did not find expected line
339
340$errorMessageExpectedLine
341
342in flutter run ${release ? '--release' : ''} stdout
343
344$copyOfListForErrorMessage
345''';
346 }
347}
348