Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 6a8d888

Browse files
authored
Provide a subset of adb logcat logs streaming during scenario_app testing (#50886)
Work towards flutter/flutter#143458. What this _doesn't_ change: 1. The full output of `adb logcat` is still written to `$FLUTTER_LOGS_DIR/logcat.txt` Example output: <img width="580" alt="Screenshot 2024-02-22 at 2 53 51�PM" src="https://github.com/flutter/engine/assets/168174/bef24370-c066-4203-ae0e-552cc0fd73ad"> /cc @dnfield @gaaclarke
1 parent fb1b0a0 commit 6a8d888

File tree

8 files changed

+1257
-40
lines changed

8 files changed

+1257
-40
lines changed

testing/scenario_app/bin/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ dart bin/android_integration_tests.dart --smoke-test dev.flutter.scenarios.Engin
2424

2525
## Additional arguments
2626

27+
- `--verbose`: Print additional information about the test run.
28+
2729
- `--adb`: The path to the `adb` tool. Defaults to
2830
`third_party/android_tools/sdk/platform-tools/adb`.
2931

testing/scenario_app/bin/android_integration_tests.dart

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'package:path/path.dart';
1414
import 'package:process/process.dart';
1515
import 'package:skia_gold_client/skia_gold_client.dart';
1616

17+
import 'utils/adb_logcat_filtering.dart';
1718
import 'utils/logs.dart';
1819
import 'utils/process_manager_extension.dart';
1920
import 'utils/screenshot_transformer.dart';
@@ -38,6 +39,11 @@ void main(List<String> args) async {
3839
help: 'Prints usage information',
3940
negatable: false,
4041
)
42+
..addFlag(
43+
'verbose',
44+
help: 'Prints verbose output',
45+
negatable: false,
46+
)
4147
..addOption(
4248
'adb',
4349
help: 'Absolute path to the adb tool',
@@ -74,8 +80,8 @@ void main(List<String> args) async {
7480
help: 'Enable Impeller for the Android app.',
7581
)
7682
..addOption('output-contents-golden',
77-
help:
78-
'Path to a file that will be used to check the contents of the output to make sure everything was created.')
83+
help: 'Path to a file that will be used to check the contents of the output to make sure everything was created.',
84+
)
7985
..addOption(
8086
'impeller-backend',
8187
help: 'The Impeller backend to use for the Android app.',
@@ -105,6 +111,7 @@ void main(List<String> args) async {
105111
panic(<String>['--adb is required']);
106112
}
107113

114+
final bool verbose = results['verbose'] as bool;
108115
final Directory outDir = Directory(results['out-dir'] as String);
109116
final File adb = File(results['adb'] as String);
110117
final bool useSkiaGold = results['use-skia-gold'] as bool;
@@ -117,6 +124,7 @@ void main(List<String> args) async {
117124
}
118125
final Directory logsDir = Directory(results['logs-dir'] as String? ?? join(outDir.path, 'scenario_app', 'logs'));
119126
await _run(
127+
verbose: verbose,
120128
outDir: outDir,
121129
adb: adb,
122130
smokeTestFullPath: smokeTest,
@@ -155,6 +163,7 @@ enum _ImpellerBackend {
155163
}
156164

157165
Future<void> _run({
166+
required bool verbose,
158167
required Directory outDir,
159168
required File adb,
160169
required String? smokeTestFullPath,
@@ -201,21 +210,29 @@ Future<void> _run({
201210
final List<Future<void>> pendingComparisons = <Future<void>>[];
202211
await step('Starting server...', () async {
203212
server = await ServerSocket.bind(InternetAddress.anyIPv4, _tcpPort);
204-
stdout.writeln('listening on host ${server.address.address}:${server.port}');
213+
if (verbose) {
214+
stdout.writeln('listening on host ${server.address.address}:${server.port}');
215+
}
205216
server.listen((Socket client) {
206-
stdout.writeln('client connected ${client.remoteAddress.address}:${client.remotePort}');
217+
if (verbose) {
218+
stdout.writeln('client connected ${client.remoteAddress.address}:${client.remotePort}');
219+
}
207220
client.transform(const ScreenshotBlobTransformer()).listen((Screenshot screenshot) {
208221
final String fileName = screenshot.filename;
209222
final Uint8List fileContent = screenshot.fileContent;
210-
log('host received ${fileContent.lengthInBytes} bytes for screenshot `$fileName`');
223+
if (verbose) {
224+
log('host received ${fileContent.lengthInBytes} bytes for screenshot `$fileName`');
225+
}
211226
assert(skiaGoldClient != null, 'expected Skia Gold client');
212227
late File goldenFile;
213228
try {
214229
goldenFile = File(join(screenshotPath, fileName))..writeAsBytesSync(fileContent, flush: true);
215230
} on FileSystemException catch (err) {
216231
panic(<String>['failed to create screenshot $fileName: $err']);
217232
}
218-
log('wrote ${goldenFile.absolute.path}');
233+
if (verbose) {
234+
log('wrote ${goldenFile.absolute.path}');
235+
}
219236
if (isSkiaGoldClientAvailable) {
220237
final Future<void> comparison = skiaGoldClient!
221238
.addImg(fileName, goldenFile,
@@ -247,8 +264,30 @@ Future<void> _run({
247264
if (exitCode != 0) {
248265
panic(<String>['could not clear logs']);
249266
}
267+
250268
logcatProcess = await pm.start(<String>[adb.path, 'logcat', '-T', '1']);
251-
logcatProcessExitCode = pipeProcessStreams(logcatProcess, out: logcat);
269+
final (Future<int> logcatExitCode, Stream<String> logcatOutput) = getProcessStreams(logcatProcess);
270+
271+
logcatProcessExitCode = logcatExitCode;
272+
logcatOutput.listen((String line) {
273+
// Always write to the full log.
274+
logcat.writeln(line);
275+
276+
// Conditionally parse and write to stderr.
277+
final AdbLogLine? adbLogLine = AdbLogLine.tryParse(line);
278+
switch (adbLogLine?.process) {
279+
case null:
280+
break;
281+
case 'ActivityManager':
282+
// TODO(matanlurey): Figure out why this isn't 'flutter.scenario' or similar.
283+
// Also, why is there two different names?
284+
case 'utter.scenario':
285+
case 'utter.scenarios':
286+
case 'flutter':
287+
case 'FlutterJNI':
288+
log('[adb] $line');
289+
}
290+
});
252291
});
253292

254293
await step('Configuring emulator...', () async {
@@ -400,6 +439,7 @@ Future<void> _run({
400439
await step('Flush logcat...', () async {
401440
await logcat.flush();
402441
await logcat.close();
442+
log('wrote logcat to $logcatPath');
403443
});
404444
}
405445
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/// Some notes about filtering `adb logcat` output, especially as a result of
2+
/// running `adb shell` to instrument the app and test scripts, as it's
3+
/// non-trivial and error-prone.
4+
///
5+
/// 1. It's probably worth keeping `ActivityManager` lines unconditionally.
6+
/// They are the most important ones, and they are not too verbose (for
7+
/// example, they don't typically contain stack traces).
8+
///
9+
/// 2. `ActivityManager` starts with the application name and process ID:
10+
///
11+
/// ```txt
12+
/// [stdout] 02-15 10:20:36.914 1735 1752 I ActivityManager: Start proc 6840:dev.flutter.scenarios/u0a98 for added application dev.flutter.scenarios
13+
/// ```
14+
///
15+
/// The "application" comes from the file `android/app/build.gradle` under
16+
/// `android > defaultConfig > applicationId`.
17+
///
18+
/// 3. Once we have the process ID, we can filter the logcat output further:
19+
///
20+
/// ```txt
21+
/// [stdout] 02-15 10:20:37.430 6840 6840 E GeneratedPluginsRegister: Tried to automatically register plugins with FlutterEngine (io.flutter.embedding.engine.FlutterEngine@144d737) but could not find or invoke the GeneratedPluginRegistrant.
22+
/// ```
23+
///
24+
/// A sample output of `adb logcat` command lives in `./sample_adb_logcat.txt`.
25+
///
26+
/// See also: <https://developer.android.com/tools/logcat>.
27+
library;
28+
29+
/// Represents a line of `adb logcat` output parsed into a structured form.
30+
///
31+
/// For example the line:
32+
/// ```txt
33+
/// 02-22 13:54:39.839 549 3683 I ActivityManager: Force stopping dev.flutter.scenarios appid=10226 user=0: start instr
34+
/// ```
35+
///
36+
/// ## Implementation notes
37+
///
38+
/// The reason this is an extension type and not a class is partially to use the
39+
/// language feature, and partially because extension types work really well
40+
/// with lazy parsing.
41+
extension type const AdbLogLine._(Match _match) {
42+
// RegEx that parses into the following groups:
43+
// 1. Everything up to the severity (I, W, E, etc.).
44+
// In other words, any whitespace, numbers, hyphens, colons, and periods.
45+
// 2. The severity (a single uppercase letter).
46+
// 3. The name of the process (up to the colon).
47+
// 4. The message (after the colon).
48+
//
49+
// This regex is simple versus being more precise. Feel free to improve it.
50+
static final RegExp _pattern = RegExp(r'([^A-Z]*)([A-Z])\s([^:]*)\:\s(.*)');
51+
52+
/// Parses the given [adbLogCatLine] into a structured form.
53+
///
54+
/// Returns `null` if the line does not match the expected format.
55+
static AdbLogLine? tryParse(String adbLogCatLine) {
56+
final Match? match = _pattern.firstMatch(adbLogCatLine);
57+
return match == null ? null : AdbLogLine._(match);
58+
}
59+
60+
/// The full line of `adb logcat` output.
61+
String get line => _match.group(0)!;
62+
63+
/// The process name, such as `ActivityManager`.
64+
String get process => _match.group(3)!;
65+
66+
/// The actual log message.
67+
String get message => _match.group(4)!;
68+
}

testing/scenario_app/bin/utils/process_manager_extension.dart

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,50 @@ import 'dart:io';
88

99
import 'package:process/process.dart';
1010

11-
/// Pipes the [process] streams and writes them to [out] sink.
12-
/// If [out] is null, then the current [Process.stdout] is used as the sink.
13-
Future<int> pipeProcessStreams(
14-
Process process, {
15-
StringSink? out,
16-
}) async {
17-
out ??= stdout;
11+
(Future<int> exitCode, Stream<String> output) getProcessStreams(
12+
Process process,
13+
) {
1814
final Completer<void> stdoutCompleter = Completer<void>();
19-
final StreamSubscription<String> stdoutSub = process.stdout
15+
final Completer<void> stderrCompleter = Completer<void>();
16+
final StreamController<String> outputController = StreamController<String>();
17+
18+
final StreamSubscription<void> stdoutSub = process.stdout
2019
.transform(utf8.decoder)
2120
.transform<String>(const LineSplitter())
22-
.listen((String line) {
23-
out!.writeln('[stdout] $line');
24-
}, onDone: stdoutCompleter.complete);
21+
.listen(outputController.add, onDone: stdoutCompleter.complete);
2522

26-
final Completer<void> stderrCompleter = Completer<void>();
27-
final StreamSubscription<String> stderrSub = process.stderr
23+
// From looking at historic logs, it seems that the stderr output is rare.
24+
// Instead of prefacing every line with [stdout] unnecessarily, we'll just
25+
// use [stderr] to indicate that it's from the stderr stream.
26+
//
27+
// For example, a historic log which has 0 occurrences of stderr:
28+
// https://gist.github.com/matanlurey/84cf9c903ef6d507dcb63d4c303ca45f
29+
final StreamSubscription<void> stderrSub = process.stderr
2830
.transform(utf8.decoder)
2931
.transform<String>(const LineSplitter())
30-
.listen((String line) {
31-
out!.writeln('[stderr] $line');
32-
}, onDone: stderrCompleter.complete);
32+
.map((String line) => '[stderr] $line')
33+
.listen(outputController.add, onDone: stderrCompleter.complete);
3334

34-
final int exitCode = await process.exitCode;
35-
await stderrSub.cancel();
36-
await stdoutSub.cancel();
35+
final Future<int> exitCode = process.exitCode.then<int>((int code) async {
36+
await (stdoutSub.cancel(), stderrSub.cancel()).wait;
37+
outputController.close();
38+
return code;
39+
});
40+
41+
return (exitCode, outputController.stream);
42+
}
43+
44+
/// Pipes the [process] streams and writes them to [out] sink.
45+
///
46+
/// If [out] is null, then the current [Process.stdout] is used as the sink.
47+
Future<int> pipeProcessStreams(
48+
Process process, {
49+
StringSink? out,
50+
}) async {
51+
out ??= stdout;
3752

38-
await stdoutCompleter.future;
39-
await stderrCompleter.future;
53+
final (Future<int> exitCode, Stream<String> output) = getProcessStreams(process);
54+
output.listen(out.writeln);
4055
return exitCode;
4156
}
4257

0 commit comments

Comments
 (0)