mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

Enables the `comment_references` and `unintended_html_in_doc_comment` lints in `packages/flutter_tool`, then fixes each of the triggering cases. This PR is test exempt due to only affecting documentation comments. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
361 lines
11 KiB
Dart
361 lines
11 KiB
Dart
// 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:flutter_tools/src/base/platform.dart';
|
|
import 'package:meta/meta.dart';
|
|
import 'package:process/process.dart';
|
|
|
|
import '../src/common.dart';
|
|
import 'test_utils.dart' show flutterBin;
|
|
|
|
const ProcessManager processManager = LocalProcessManager();
|
|
final String flutterRoot = getFlutterRoot();
|
|
|
|
void debugPrint(String message) {
|
|
// This is called to intentionally print debugging output when a test is
|
|
// either taking too long or has failed.
|
|
// ignore: avoid_print
|
|
print(message);
|
|
}
|
|
|
|
typedef LineHandler = String? Function(String line);
|
|
|
|
abstract class Transition {
|
|
const Transition({this.handler, this.logging});
|
|
|
|
/// Callback that is invoked when the transition matches.
|
|
///
|
|
/// This should not throw, even if the test is failing. (For example, don't use "expect"
|
|
/// in these callbacks.) Throwing here would prevent the [runFlutter] function from running
|
|
/// to completion, which would leave zombie `flutter` processes around.
|
|
final LineHandler? handler;
|
|
|
|
/// Whether to enable or disable logging when this transition is matched.
|
|
///
|
|
/// The default value, null, leaves the logging state unaffected.
|
|
final bool? logging;
|
|
|
|
bool matches(String line);
|
|
|
|
@protected
|
|
bool lineMatchesPattern(String line, Pattern pattern, bool contains) {
|
|
if (pattern is RegExp) {
|
|
// Ideally this would also distinguish between "contains" and "equals"
|
|
// operation.
|
|
return line.contains(pattern);
|
|
}
|
|
return contains ? line.contains(pattern) : line == pattern;
|
|
}
|
|
|
|
@protected
|
|
String describe(Pattern pattern, bool contains) {
|
|
if (pattern is RegExp) {
|
|
return '/${pattern.pattern}/';
|
|
}
|
|
return contains ? '"...$pattern..."' : '"$pattern"';
|
|
}
|
|
}
|
|
|
|
class Barrier extends Transition {
|
|
Barrier(this.pattern, {super.handler, super.logging}) : contains = false;
|
|
Barrier.contains(this.pattern, {super.handler, super.logging}) : contains = true;
|
|
|
|
final Pattern pattern;
|
|
final bool contains;
|
|
|
|
@override
|
|
bool matches(String line) => lineMatchesPattern(line, pattern, contains);
|
|
|
|
@override
|
|
String toString() => describe(pattern, contains);
|
|
}
|
|
|
|
class Multiple extends Transition {
|
|
Multiple(List<Pattern> patterns, {super.handler, super.logging})
|
|
: _originalPatterns = patterns,
|
|
patterns = patterns.toList(),
|
|
contains = false;
|
|
Multiple.contains(List<Pattern> patterns, {super.handler, super.logging})
|
|
: _originalPatterns = patterns,
|
|
patterns = patterns.toList(),
|
|
contains = true;
|
|
|
|
final List<Pattern> _originalPatterns;
|
|
final List<Pattern> patterns;
|
|
final bool contains;
|
|
|
|
@override
|
|
bool matches(String line) {
|
|
for (int index = 0; index < patterns.length; index += 1) {
|
|
if (lineMatchesPattern(line, patterns[index], contains)) {
|
|
patterns.removeAt(index);
|
|
break;
|
|
}
|
|
}
|
|
return patterns.isEmpty;
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
String describe(Pattern pattern) => super.describe(pattern, contains);
|
|
if (patterns.isEmpty) {
|
|
return '${_originalPatterns.map(describe).join(', ')} (all matched)';
|
|
}
|
|
return '${_originalPatterns.map(describe).join(', ')} (matched ${_originalPatterns.length - patterns.length} so far)';
|
|
}
|
|
}
|
|
|
|
class LogLine {
|
|
const LogLine(this.channel, this.stamp, this.message);
|
|
final String channel;
|
|
final String stamp;
|
|
final String message;
|
|
|
|
bool get couldBeCrash => message.contains('Oops; flutter has exited unexpectedly:');
|
|
|
|
@override
|
|
String toString() => '$stamp $channel: $message';
|
|
|
|
void printClearly() {
|
|
debugPrint('$stamp $channel: ${clarify(message)}');
|
|
}
|
|
|
|
static String clarify(String line) {
|
|
return line.runes
|
|
.map<String>(
|
|
(int rune) => switch (rune) {
|
|
>= 0x20 && <= 0x7F => String.fromCharCode(rune),
|
|
0x00 => '<NUL>',
|
|
0x07 => '<BEL>',
|
|
0x08 => '<TAB>',
|
|
0x09 => '<BS>',
|
|
0x0A => '<LF>',
|
|
0x0D => '<CR>',
|
|
_ =>
|
|
'<${rune.toRadixString(16).padLeft(rune <= 0xFF
|
|
? 2
|
|
: rune <= 0xFFFF
|
|
? 4
|
|
: 5, '0')}>',
|
|
},
|
|
)
|
|
.join();
|
|
}
|
|
}
|
|
|
|
class ProcessTestResult {
|
|
const ProcessTestResult(this.exitCode, this.logs);
|
|
final int exitCode;
|
|
final List<LogLine> logs;
|
|
|
|
List<String> get stdout {
|
|
return logs
|
|
.where((LogLine log) => log.channel == 'stdout')
|
|
.map<String>((LogLine log) => log.message)
|
|
.toList();
|
|
}
|
|
|
|
List<String> get stderr {
|
|
return logs
|
|
.where((LogLine log) => log.channel == 'stderr')
|
|
.map<String>((LogLine log) => log.message)
|
|
.toList();
|
|
}
|
|
|
|
@override
|
|
String toString() => 'exit code $exitCode\nlogs:\n ${logs.join('\n ')}\n';
|
|
}
|
|
|
|
Future<ProcessTestResult> runFlutter(
|
|
List<String> arguments,
|
|
String workingDirectory,
|
|
List<Transition> transitions, {
|
|
bool debug = false,
|
|
bool logging = true,
|
|
Duration expectedMaxDuration = const Duration(
|
|
minutes: 10,
|
|
), // must be less than test timeout of 15 minutes! See ../../dart_test.yaml.
|
|
}) async {
|
|
const LocalPlatform platform = LocalPlatform();
|
|
final Stopwatch clock = Stopwatch()..start();
|
|
final Process process = await processManager.start(<String>[
|
|
// In a container with no X display, use the virtual framebuffer.
|
|
if (platform.isLinux && (platform.environment['DISPLAY'] ?? '').isEmpty) '/usr/bin/xvfb-run',
|
|
flutterBin,
|
|
...arguments,
|
|
], workingDirectory: workingDirectory);
|
|
final List<LogLine> logs = <LogLine>[];
|
|
int nextTransition = 0;
|
|
void describeStatus() {
|
|
if (transitions.isNotEmpty) {
|
|
debugPrint('Expected state transitions:');
|
|
for (int index = 0; index < transitions.length; index += 1) {
|
|
debugPrint(
|
|
'${index.toString().padLeft(5)} '
|
|
'${index < nextTransition
|
|
? 'ALREADY MATCHED '
|
|
: index == nextTransition
|
|
? 'NOW WAITING FOR>'
|
|
: ' '} ${transitions[index]}',
|
|
);
|
|
}
|
|
}
|
|
if (logs.isEmpty) {
|
|
debugPrint(
|
|
'So far nothing has been logged${debug ? "" : "; use debug:true to print all output"}.',
|
|
);
|
|
} else {
|
|
debugPrint(
|
|
'Log${debug ? "" : " (only contains logged lines; use debug:true to print all output)"}:',
|
|
);
|
|
for (final LogLine log in logs) {
|
|
log.printClearly();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool streamingLogs = false;
|
|
Timer? timeout;
|
|
void processTimeout() {
|
|
if (!streamingLogs) {
|
|
streamingLogs = true;
|
|
if (!debug) {
|
|
debugPrint('Test is taking a long time (${clock.elapsed.inSeconds} seconds so far).');
|
|
}
|
|
describeStatus();
|
|
debugPrint('(streaming all logs from this point on...)');
|
|
} else {
|
|
debugPrint('(taking a long time...)');
|
|
}
|
|
}
|
|
|
|
String stamp() => '[${(clock.elapsed.inMilliseconds / 1000.0).toStringAsFixed(1).padLeft(5)}s]';
|
|
void processStdout(String line) {
|
|
final LogLine log = LogLine('stdout', stamp(), line);
|
|
if (logging) {
|
|
logs.add(log);
|
|
}
|
|
if (streamingLogs) {
|
|
log.printClearly();
|
|
}
|
|
if (nextTransition < transitions.length && transitions[nextTransition].matches(line)) {
|
|
if (streamingLogs) {
|
|
debugPrint('(matched ${transitions[nextTransition]})');
|
|
}
|
|
if (transitions[nextTransition].logging != null) {
|
|
if (!logging && transitions[nextTransition].logging!) {
|
|
logs.add(log);
|
|
}
|
|
logging = transitions[nextTransition].logging!;
|
|
if (streamingLogs) {
|
|
if (logging) {
|
|
debugPrint('(enabled logging)');
|
|
} else {
|
|
debugPrint('(disabled logging)');
|
|
}
|
|
}
|
|
}
|
|
if (transitions[nextTransition].handler != null) {
|
|
final String? command = transitions[nextTransition].handler!(line);
|
|
if (command != null) {
|
|
final LogLine inLog = LogLine('stdin', stamp(), command);
|
|
logs.add(inLog);
|
|
if (streamingLogs) {
|
|
inLog.printClearly();
|
|
}
|
|
process.stdin.write(command);
|
|
}
|
|
}
|
|
nextTransition += 1;
|
|
timeout?.cancel();
|
|
timeout = Timer(
|
|
expectedMaxDuration ~/ 5,
|
|
processTimeout,
|
|
); // This is not a failure timeout, just when to start logging verbosely to help debugging.
|
|
}
|
|
}
|
|
|
|
void processStderr(String line) {
|
|
final LogLine log = LogLine('stdout', stamp(), line);
|
|
logs.add(log);
|
|
if (streamingLogs) {
|
|
log.printClearly();
|
|
}
|
|
}
|
|
|
|
if (debug) {
|
|
processTimeout();
|
|
} else {
|
|
timeout = Timer(
|
|
expectedMaxDuration ~/ 2,
|
|
processTimeout,
|
|
); // This is not a failure timeout, just when to start logging verbosely to help debugging.
|
|
}
|
|
process.stdout
|
|
.transform<String>(utf8.decoder)
|
|
.transform<String>(const LineSplitter())
|
|
.listen(processStdout);
|
|
process.stderr
|
|
.transform<String>(utf8.decoder)
|
|
.transform<String>(const LineSplitter())
|
|
.listen(processStderr);
|
|
unawaited(
|
|
process.exitCode
|
|
.timeout(
|
|
expectedMaxDuration,
|
|
onTimeout: () {
|
|
// This is a failure timeout, must not be short.
|
|
debugPrint(
|
|
'${stamp()} (process is not quitting, trying to send a "q" just in case that helps)',
|
|
);
|
|
debugPrint('(a functional test should never reach this point)');
|
|
final LogLine inLog = LogLine('stdin', stamp(), 'q');
|
|
logs.add(inLog);
|
|
if (streamingLogs) {
|
|
inLog.printClearly();
|
|
}
|
|
process.stdin.write('q');
|
|
return -1; // discarded
|
|
},
|
|
)
|
|
.then(
|
|
(int i) => i,
|
|
onError: (Object error) {
|
|
// ignore errors here, they will be reported on the next line
|
|
return -1; // discarded
|
|
},
|
|
),
|
|
);
|
|
final int exitCode = await process.exitCode;
|
|
if (streamingLogs) {
|
|
debugPrint('${stamp()} (process terminated with exit code $exitCode)');
|
|
}
|
|
timeout?.cancel();
|
|
if (nextTransition < transitions.length) {
|
|
debugPrint('The subprocess terminated before all the expected transitions had been matched.');
|
|
if (logs.any((LogLine line) => line.couldBeCrash)) {
|
|
debugPrint('The subprocess may in fact have crashed. Check the stderr logs below.');
|
|
}
|
|
debugPrint('The transition that we were hoping to see next but that we never saw was:');
|
|
debugPrint(
|
|
'${nextTransition.toString().padLeft(5)} NOW WAITING FOR> ${transitions[nextTransition]}',
|
|
);
|
|
if (!streamingLogs) {
|
|
describeStatus();
|
|
debugPrint('(process terminated with exit code $exitCode)');
|
|
}
|
|
throw TestFailure('Missed some expected transitions.');
|
|
}
|
|
if (streamingLogs) {
|
|
debugPrint('${stamp()} (completed execution successfully!)');
|
|
}
|
|
return ProcessTestResult(exitCode, logs);
|
|
}
|
|
|
|
const int progressMessageWidth = 64;
|