Unverified Commit 76f70810 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

[O] Remove many timeouts. (#23531)

* Remove many timeouts.

These are essentially self-inflicted race conditions. Instead of timeouts we're going to try a more verbose logging mechanism that points out when things are taking a long time.

* Get the attach tests to pass.

* Apply review comments from Todd

* More review comment fixes

* Put back the extended timeouts here now that I know why we have them...
parent b3b764c9
......@@ -50,20 +50,22 @@ void main() {
'--verbose',
'-d',
device.deviceId,
'lib/commands.dart'
'lib/commands.dart',
],
);
final StreamController<String> stdout =
StreamController<String>.broadcast();
final StreamController<String> stdout = StreamController<String>.broadcast();
transformToLines(run.stdout).listen((String line) {
print('run:stdout: $line');
stdout.add(line);
final dynamic json = parseFlutterResponse(line);
if (json != null && json['event'] == 'app.debugPort') {
vmServicePort = Uri.parse(json['params']['wsUri']).port;
print('service protocol connection available at port $vmServicePort');
} else if (json != null && json['event'] == 'app.started') {
appId = json['params']['appId'];
if (json != null) {
if (json['event'] == 'app.debugPort') {
vmServicePort = Uri.parse(json['params']['wsUri']).port;
print('service protocol connection available at port $vmServicePort');
} else if (json['event'] == 'app.started') {
appId = json['params']['appId'];
print('application identifier is $appId');
}
}
if (vmServicePort != null && appId != null && !ready.isCompleted) {
print('run: ready!');
......@@ -73,6 +75,7 @@ void main() {
});
transformToLines(run.stderr).listen((String line) {
stderr.writeln('run:stderr: $line');
ok = false;
});
run.exitCode.then<void>((int exitCode) {
ok = false;
......@@ -81,17 +84,15 @@ void main() {
if (!ok)
throw 'Failed to run test app.';
final VMServiceClient client =
VMServiceClient.connect('ws://localhost:$vmServicePort/ws');
final VMServiceClient client = VMServiceClient.connect(
'ws://localhost:$vmServicePort/ws'
);
int id = 1;
Future<Map<String, dynamic>> sendRequest(
String method, dynamic params) async {
Future<Map<String, dynamic>> sendRequest(String method, dynamic params) async {
final int requestId = id++;
final Completer<Map<String, dynamic>> response =
Completer<Map<String, dynamic>>();
final StreamSubscription<String> responseSubscription =
stdout.stream.listen((String line) {
final Completer<Map<String, dynamic>> response = Completer<Map<String, dynamic>>();
final StreamSubscription<String> responseSubscription = stdout.stream.listen((String line) {
final Map<String, dynamic> json = parseFlutterResponse(line);
if (json != null && json['id'] == requestId)
response.complete(json);
......@@ -110,27 +111,35 @@ void main() {
}
print('test: sending two hot reloads...');
final Future<dynamic> hotReload1 = sendRequest('app.restart',
<String, dynamic>{'appId': appId, 'fullRestart': false});
final Future<dynamic> hotReload2 = sendRequest('app.restart',
<String, dynamic>{'appId': appId, 'fullRestart': false});
final Future<List<dynamic>> reloadRequests =
Future.wait<dynamic>(<Future<dynamic>>[hotReload1, hotReload2]);
final dynamic results = await Future
.any<dynamic>(<Future<dynamic>>[run.exitCode, reloadRequests]);
final Future<dynamic> hotReload1 = sendRequest(
'app.restart',
<String, dynamic>{'appId': appId, 'fullRestart': false},
);
final Future<dynamic> hotReload2 = sendRequest(
'app.restart',
<String, dynamic>{'appId': appId, 'fullRestart': false},
);
final Future<List<dynamic>> reloadRequests = Future.wait<dynamic>(<Future<dynamic>>[
hotReload1,
hotReload2,
]);
final dynamic results = await Future.any<dynamic>(<Future<dynamic>>[
run.exitCode,
reloadRequests,
]);
if (!ok)
throw 'App crashed during hot reloads.';
throw 'App failed or crashed during hot reloads.';
final List<dynamic> responses = results;
final List<dynamic> errorResponses =
responses.where((dynamic r) => r['error'] != null).toList();
final List<dynamic> successResponses = responses
.where((dynamic r) =>
r['error'] == null &&
r['result'] != null &&
r['result']['code'] == 0)
.toList();
final List<dynamic> errorResponses = responses.where(
(dynamic r) => r['error'] != null
).toList();
final List<dynamic> successResponses = responses.where(
(dynamic r) => r['error'] == null &&
r['result'] != null &&
r['result']['code'] == 0
).toList();
if (errorResponses.length != 1)
throw 'Did not receive the expected (exactly one) hot reload error response.';
......@@ -140,8 +149,10 @@ void main() {
if (successResponses.length != 1)
throw 'Did not receive the expected (exactly one) successful hot reload response.';
final dynamic hotReload3 = await sendRequest('app.restart',
<String, dynamic>{'appId': appId, 'fullRestart': false});
final dynamic hotReload3 = await sendRequest(
'app.restart',
<String, dynamic>{'appId': appId, 'fullRestart': false},
);
if (hotReload3['error'] != null)
throw 'Received an error response from a hot reload after all other hot reloads had completed.';
......@@ -150,7 +161,7 @@ void main() {
if (result != 0)
throw 'Received unexpected exit code $result from run process.';
print('test: validating that the app has in fact closed...');
await client.done.timeout(const Duration(seconds: 5));
await client.done;
});
return TaskResult.success(null);
});
......
......@@ -29,7 +29,7 @@ Future<void> main() async {
final SerializableFinder summary = find.byValueKey('summary');
// Wait for calibration to complete and fab to appear.
await driver.waitFor(fab, timeout: const Duration(seconds: 40));
await driver.waitFor(fab);
final String calibrationResult = await driver.getText(summary);
final Match matchCalibration = calibrationRegExp.matchAsPrefix(calibrationResult);
......@@ -59,7 +59,7 @@ Future<void> main() async {
expect(double.parse(matchFast.group(1)), closeTo(flutterFrameRate * 2.0, 5.0));
expect(double.parse(matchFast.group(2)), closeTo(flutterFrameRate, 10.0));
expect(int.parse(matchFast.group(3)), 1);
}, timeout: const Timeout(Duration(minutes: 1)));
});
tearDownAll(() async {
driver?.close();
......
......@@ -9,16 +9,24 @@ import 'package:meta/meta.dart';
abstract class Command {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const Command({ Duration timeout })
: timeout = timeout ?? const Duration(seconds: 5);
const Command({ this.timeout });
/// Deserializes this command from the value generated by [serialize].
Command.deserialize(Map<String, String> json)
: timeout = Duration(milliseconds: int.parse(json['timeout']));
: timeout = _parseTimeout(json);
static Duration _parseTimeout(Map<String, String> json) {
final String timeout = json['timeout'];
if (timeout == null)
return null;
return Duration(milliseconds: int.parse(timeout));
}
/// The maximum amount of time to wait for the command to complete.
///
/// Defaults to 5 seconds.
/// Defaults to no timeout, because it is common for operations to take oddly
/// long in test environments (e.g. because the test host is overloaded), and
/// having timeouts essentially means having race conditions.
final Duration timeout;
/// Identifies the type of the command object and of the handler.
......@@ -28,7 +36,7 @@ abstract class Command {
@mustCallSuper
Map<String, String> serialize() => <String, String>{
'command': kind,
'timeout': '${timeout.inMilliseconds}',
'timeout': timeout == null ? null : '${timeout.inMilliseconds}',
};
}
......
......@@ -177,7 +177,10 @@ class FlutterDriverExtension {
if (commandHandler == null || commandDeserializer == null)
throw 'Extension $_extensionMethod does not support command $commandKind';
final Command command = commandDeserializer(params);
final Result response = await commandHandler(command).timeout(command.timeout);
Future<Result> responseFuture = commandHandler(command);
if (command.timeout != null)
responseFuture = responseFuture.timeout(command.timeout);
final Result response = await responseFuture;
return _makeResponse(response?.toJson());
} on TimeoutException catch (error, stackTrace) {
final String msg = 'Timeout while executing $commandKind: $error\n$stackTrace';
......
......@@ -11,13 +11,13 @@ import 'package:flutter_driver/src/driver/timeline.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:mockito/mockito.dart';
import 'package:vm_service_client/vm_service_client.dart';
import 'package:quiver/testing/async.dart';
import 'common.dart';
/// Magical timeout value that's different from the default.
const Duration _kTestTimeout = Duration(milliseconds: 1234);
const String _kSerializedTestTimeout = '1234';
const Duration _kDefaultCommandTimeout = Duration(seconds: 5);
void main() {
group('FlutterDriver.connect', () {
......@@ -358,17 +358,19 @@ void main() {
group('sendCommand error conditions', () {
test('local timeout', () async {
final List<String> log = <String>[];
final StreamSubscription<LogRecord> logSub = flutterDriverLog.listen((LogRecord s) => log.add(s.toString()));
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
// completer never competed to trigger timeout
// completer never completed to trigger timeout
return Completer<Map<String, dynamic>>().future;
});
try {
await driver.waitFor(find.byTooltip('foo'), timeout: const Duration(milliseconds: 100));
fail('expected an exception');
} catch (error) {
expect(error is DriverError, isTrue);
expect(error.message, 'Failed to fulfill WaitFor: Flutter application not responding');
}
FakeAsync().run((FakeAsync time) {
driver.waitFor(find.byTooltip('foo'));
expect(log, <String>[]);
time.elapse(const Duration(hours: 1));
});
expect(log, <String>['[warning] FlutterDriver: waitFor message is taking a long time to complete...']);
await logSub.cancel();
});
test('remote error', () async {
......@@ -389,7 +391,6 @@ void main() {
});
group('FlutterDriver with custom timeout', () {
const double kTestMultiplier = 3.0;
MockVMServiceClient mockClient;
MockPeer mockPeer;
MockIsolate mockIsolate;
......@@ -399,21 +400,21 @@ void main() {
mockClient = MockVMServiceClient();
mockPeer = MockPeer();
mockIsolate = MockIsolate();
driver = FlutterDriver.connectedTo(mockClient, mockPeer, mockIsolate, timeoutMultiplier: kTestMultiplier);
driver = FlutterDriver.connectedTo(mockClient, mockPeer, mockIsolate);
});
test('multiplies the timeout', () async {
test('GetHealth has no default timeout', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, String>{
'command': 'get_health',
'timeout': '${(_kDefaultCommandTimeout * kTestMultiplier).inMilliseconds}',
'timeout': null,
});
return makeMockResponse(<String, dynamic>{'status': 'ok'});
});
await driver.checkHealth();
});
test('does not multiply explicit timeouts', () async {
test('does not interfere with explicit timeouts', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, String>{
'command': 'get_health',
......
......@@ -88,15 +88,13 @@ class AndroidDevice extends Device {
propCommand,
stdoutEncoding: latin1,
stderrEncoding: latin1,
).timeout(const Duration(seconds: 5));
);
if (result.exitCode == 0) {
_properties = parseAdbDeviceProperties(result.stdout);
} else {
printError('Error retrieving device properties for $name:');
printError(result.stderr);
}
} on TimeoutException catch (_) {
throwToolExit('adb not responding');
} on ProcessException catch (error) {
printError('Error retrieving device properties for $name: $error');
}
......@@ -279,7 +277,7 @@ class AndroidDevice extends Device {
if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
return false;
final Status status = logger.startProgress('Installing ${fs.path.relative(apk.file.path)}...', expectSlowOperation: true);
final Status status = logger.startProgress('Installing ${fs.path.relative(apk.file.path)}...', timeout: kSlowOperation);
final RunResult installResult = await runAsync(adbCommandForDevice(<String>['install', '-t', '-r', apk.file.path]));
status.stop();
// Some versions of adb exit with exit code 0 even on failure :(
......
......@@ -51,9 +51,10 @@ class AndroidEmulator extends Emulator {
throw '${runResult.stdout}\n${runResult.stderr}'.trimRight();
}
});
// emulator continues running on a successful launch so if we
// haven't quit within 3 seconds we assume that's a success and just
// return.
// The emulator continues running on a successful launch, so if it hasn't
// quit within 3 seconds we assume that's a success and just return. This
// means that on a slow machine, a failure that takes more than three
// seconds won't be recognized as such... :-/
return Future.any<void>(<Future<void>>[
launchResult,
Future<void>.delayed(const Duration(seconds: 3))
......
......@@ -50,32 +50,41 @@ class AndroidWorkflow implements Workflow {
class AndroidValidator extends DoctorValidator {
AndroidValidator(): super('Android toolchain - develop for Android devices',);
@override
String get slowWarning => '${_task ?? 'This'} is taking a long time...';
String _task;
/// Returns false if we cannot determine the Java version or if the version
/// is not compatible.
Future<bool> _checkJavaVersion(String javaBinary, List<ValidationMessage> messages) async {
if (!processManager.canRun(javaBinary)) {
messages.add(ValidationMessage.error(userMessages.androidCantRunJavaBinary(javaBinary)));
return false;
}
String javaVersion;
_task = 'Checking Java status';
try {
printTrace('java -version');
final ProcessResult result = await processManager.run(<String>[javaBinary, '-version']);
if (result.exitCode == 0) {
final List<String> versionLines = result.stderr.split('\n');
javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
if (!processManager.canRun(javaBinary)) {
messages.add(ValidationMessage.error(userMessages.androidCantRunJavaBinary(javaBinary)));
return false;
}
} catch (error) {
printTrace(error.toString());
}
if (javaVersion == null) {
// Could not determine the java version.
messages.add(ValidationMessage.error(userMessages.androidUnknownJavaVersion));
return false;
String javaVersion;
try {
printTrace('java -version');
final ProcessResult result = await processManager.run(<String>[javaBinary, '-version']);
if (result.exitCode == 0) {
final List<String> versionLines = result.stderr.split('\n');
javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
}
} catch (error) {
printTrace(error.toString());
}
if (javaVersion == null) {
// Could not determine the java version.
messages.add(ValidationMessage.error(userMessages.androidUnknownJavaVersion));
return false;
}
messages.add(ValidationMessage(userMessages.androidJavaVersion(javaVersion)));
// TODO(johnmccutchan): Validate version.
return true;
} finally {
_task = null;
}
messages.add(ValidationMessage(userMessages.androidJavaVersion(javaVersion)));
// TODO(johnmccutchan): Validate version.
return true;
}
@override
......@@ -149,6 +158,9 @@ class AndroidValidator extends DoctorValidator {
class AndroidLicenseValidator extends DoctorValidator {
AndroidLicenseValidator(): super('Android license subvalidator',);
@override
String get slowWarning => 'Checking Android licenses is taking an unexpectedly long time...';
@override
Future<ValidationResult> validate() async {
final List<ValidationMessage> messages = <ValidationMessage>[];
......@@ -208,10 +220,8 @@ class AndroidLicenseValidator extends DoctorValidator {
Future<LicensesAccepted> get licensesAccepted async {
LicensesAccepted status;
void _onLine(String line) {
if (status == null && licenseAccepted.hasMatch(line)) {
status = LicensesAccepted.all;
} else if (licenseCounts.hasMatch(line)) {
void _handleLine(String line) {
if (licenseCounts.hasMatch(line)) {
final Match match = licenseCounts.firstMatch(line);
if (match.group(1) != match.group(2)) {
status = LicensesAccepted.some;
......@@ -219,9 +229,12 @@ class AndroidLicenseValidator extends DoctorValidator {
status = LicensesAccepted.none;
}
} else if (licenseNotAccepted.hasMatch(line)) {
// In case the format changes, a more general match will keep doctor
// mostly working.
// The licenseNotAccepted pattern is trying to match the same line as
// licenseCounts, but is more general. In case the format changes, a
// more general match may keep doctor mostly working.
status = LicensesAccepted.none;
} else if (licenseAccepted.hasMatch(line)) {
status ??= LicensesAccepted.all;
}
}
......@@ -235,19 +248,14 @@ class AndroidLicenseValidator extends DoctorValidator {
final Future<void> output = process.stdout
.transform<String>(const Utf8Decoder(allowMalformed: true))
.transform<String>(const LineSplitter())
.listen(_onLine)
.listen(_handleLine)
.asFuture<void>(null);
final Future<void> errors = process.stderr
.transform<String>(const Utf8Decoder(allowMalformed: true))
.transform<String>(const LineSplitter())
.listen(_onLine)
.listen(_handleLine)
.asFuture<void>(null);
try {
await Future.wait<void>(<Future<void>>[output, errors]).timeout(const Duration(seconds: 30));
} catch (TimeoutException) {
printTrace(userMessages.androidLicensesTimeout(androidSdk.sdkManagerPath));
processManager.killPid(process.pid);
}
await Future.wait<void>(<Future<void>>[output, errors]);
return status ?? LicensesAccepted.unknown;
}
......@@ -261,9 +269,10 @@ class AndroidLicenseValidator extends DoctorValidator {
_ensureCanRunSdkManager();
final Version sdkManagerVersion = Version.parse(androidSdk.sdkManagerVersion);
if (sdkManagerVersion == null || sdkManagerVersion.major < 26)
if (sdkManagerVersion == null || sdkManagerVersion.major < 26) {
// SDK manager is found, but needs to be updated.
throwToolExit(userMessages.androidSdkOutdated(androidSdk.sdkManagerPath));
}
final Process process = await runCommand(
<String>[androidSdk.sdkManagerPath, '--licenses'],
......
......@@ -97,7 +97,7 @@ Future<GradleProject> _readGradleProject() async {
final FlutterProject flutterProject = await FlutterProject.current();
final String gradle = await _ensureGradle(flutterProject);
updateLocalProperties(project: flutterProject);
final Status status = logger.startProgress('Resolving dependencies...', expectSlowOperation: true);
final Status status = logger.startProgress('Resolving dependencies...', timeout: kSlowOperation);
GradleProject project;
try {
final RunResult propertiesRunResult = await runCheckedAsync(
......@@ -174,7 +174,7 @@ Future<String> _ensureGradle(FlutterProject project) async {
// of validating the Gradle executable. This may take several seconds.
Future<String> _initializeGradle(FlutterProject project) async {
final Directory android = project.android.hostAppGradleRoot;
final Status status = logger.startProgress('Initializing gradle...', expectSlowOperation: true);
final Status status = logger.startProgress('Initializing gradle...', timeout: kSlowOperation);
String gradle = _locateGradlewExecutable(android);
if (gradle == null) {
injectGradleWrapper(android);
......@@ -312,8 +312,8 @@ Future<void> buildGradleProject({
Future<void> _buildGradleProjectV1(FlutterProject project, String gradle) async {
// Run 'gradlew build'.
final Status status = logger.startProgress(
"Running 'gradlew build'...",
expectSlowOperation: true,
'Running \'gradlew build\'...',
timeout: kSlowOperation,
multilineOutput: true,
);
final int exitCode = await runCommandAndStreamOutput(
......@@ -354,8 +354,8 @@ Future<void> _buildGradleProjectV2(
}
}
final Status status = logger.startProgress(
"Gradle task '$assembleTask'...",
expectSlowOperation: true,
'Running Gradle task \'$assembleTask\'...',
timeout: kSlowOperation,
multilineOutput: true,
);
final String gradlePath = fs.file(gradle).absolute.path;
......
......@@ -107,7 +107,7 @@ class AppContext {
/// Gets the value associated with the specified [type], or `null` if no
/// such value has been associated.
dynamic operator [](Type type) {
Object operator [](Type type) {
dynamic value = _generateIfNecessary(type, _overrides);
if (value == null && _parent != null)
value = _parent[type];
......
......@@ -36,9 +36,7 @@ RecordingFileSystem getRecordingFileSystem(String location) {
final RecordingFileSystem fileSystem = RecordingFileSystem(
delegate: _kLocalFs, destination: dir);
addShutdownHook(() async {
await fileSystem.recording.flush(
pendingResultTimeout: const Duration(seconds: 5),
);
await fileSystem.recording.flush();
}, ShutdownStage.SERIALIZE_RECORDING);
return fileSystem;
}
......
......@@ -162,10 +162,7 @@ class Stdio {
bool get supportsAnsiEscapes => hasTerminal ? io.stdout.supportsAnsiEscapes : false;
}
io.IOSink get stderr => context[Stdio].stderr;
Stream<List<int>> get stdin => context[Stdio].stdin;
io.IOSink get stdout => context[Stdio].stdout;
Stdio get stdio => context[Stdio];
io.IOSink get stdout => stdio.stdout;
Stream<List<int>> get stdin => stdio.stdin;
io.IOSink get stderr => stdio.stderr;
......@@ -37,7 +37,7 @@ Future<List<int>> _attempt(Uri url, {bool onlyHeaders = false}) async {
printTrace('Downloading: $url');
HttpClient httpClient;
if (context[HttpClientFactory] != null) {
httpClient = context[HttpClientFactory]();
httpClient = (context[HttpClientFactory] as HttpClientFactory)(); // ignore: avoid_as
} else {
httpClient = HttpClient();
}
......
......@@ -203,14 +203,6 @@ Future<int> runInteractively(List<String> command, {
return await process.exitCode;
}
Future<void> runAndKill(List<String> cmd, Duration timeout) {
final Future<Process> proc = runDetached(cmd);
return Future<void>.delayed(timeout, () async {
printTrace('Intentionally killing ${cmd[0]}');
processManager.killPid((await proc).pid);
});
}
Future<Process> runDetached(List<String> cmd) {
_traceCommand(cmd);
final Future<Process> proc = processManager.start(
......
......@@ -5,8 +5,6 @@
import 'dart:async';
import 'dart:convert' show AsciiDecoder;
import 'package:quiver/strings.dart';
import '../globals.dart';
import 'context.dart';
import 'io.dart' as io;
......@@ -172,31 +170,32 @@ class AnsiTerminal {
/// Return keystrokes from the console.
///
/// Useful when the console is in [singleCharMode].
Stream<String> get onCharInput {
Stream<String> get keystrokes {
_broadcastStdInString ??= io.stdin.transform<String>(const AsciiDecoder(allowInvalid: true)).asBroadcastStream();
return _broadcastStdInString;
}
/// Prompts the user to input a character within the accepted list. Re-prompts
/// if entered character is not in the list.
/// Prompts the user to input a character within a given list. Re-prompts if
/// entered character is not in the list.
///
/// The [prompt] is the text displayed prior to waiting for user input. The
/// [defaultChoiceIndex], if given, will be the character appearing in
/// [acceptedCharacters] in the index given if the user presses enter without
/// any key input. Setting [displayAcceptedCharacters] also prints the
/// accepted keys next to the [prompt].
/// The `prompt`, if non-null, is the text displayed prior to waiting for user
/// input each time. If `prompt` is non-null and `displayAcceptedCharacters`
/// is true, the accepted keys are printed next to the `prompt`.
///
/// Throws a [TimeoutException] if a `timeout` is provided and its duration
/// expired without user input. Duration resets per key press.
/// The returned value is the user's input; if `defaultChoiceIndex` is not
/// null, and the user presses enter without any other input, the return value
/// will be the character in `acceptedCharacters` at the index given by
/// `defaultChoiceIndex`.
Future<String> promptForCharInput(
List<String> acceptedCharacters, {
String prompt,
int defaultChoiceIndex,
bool displayAcceptedCharacters = true,
Duration timeout,
}) async {
assert(acceptedCharacters != null);
assert(acceptedCharacters.isNotEmpty);
assert(prompt == null || prompt.isNotEmpty);
assert(displayAcceptedCharacters != null);
List<String> charactersToDisplay = acceptedCharacters;
if (defaultChoiceIndex != null) {
assert(defaultChoiceIndex >= 0 && defaultChoiceIndex < acceptedCharacters.length);
......@@ -206,17 +205,14 @@ class AnsiTerminal {
}
String choice;
singleCharMode = true;
while (isEmpty(choice) || choice.length != 1 || !acceptedCharacters.contains(choice)) {
if (isNotEmpty(prompt)) {
while (choice == null || choice.length > 1 || !acceptedCharacters.contains(choice)) {
if (prompt != null) {
printStatus(prompt, emphasis: true, newline: false);
if (displayAcceptedCharacters)
printStatus(' [${charactersToDisplay.join("|")}]', newline: false);
printStatus(': ', emphasis: true, newline: false);
}
Future<String> inputFuture = onCharInput.first;
if (timeout != null)
inputFuture = inputFuture.timeout(timeout);
choice = await inputFuture;
choice = await keystrokes.first;
printStatus(choice);
}
singleCharMode = false;
......
......@@ -299,7 +299,7 @@ abstract class CachedArtifact {
Future<void> _downloadArchive(String message, Uri url, Directory location, bool verifier(File f), void extractor(File f, Directory d)) {
return _withDownloadFile('${flattenNameSubdirs(url)}', (File tempFile) async {
if (!verifier(tempFile)) {
final Status status = logger.startProgress(message, expectSlowOperation: true);
final Status status = logger.startProgress(message, timeout: kSlowOperation);
try {
await _downloadFile(url, tempFile);
status.stop();
......@@ -620,7 +620,7 @@ Future<void> _downloadFile(Uri url, File location) async {
}
Future<bool> _doesRemoteExist(String message, Uri url) async {
final Status status = logger.startProgress(message, expectSlowOperation: true);
final Status status = logger.startProgress(message, timeout: kSlowOperation);
final bool exists = await doesRemoteFileExist(url);
status.stop();
return exists;
......
......@@ -74,11 +74,12 @@ class AnalyzeContinuously extends AnalyzeBase {
analysisStatus?.cancel();
if (!firstAnalysis)
printStatus('\n');
analysisStatus = logger.startProgress('Analyzing $analysisTarget...');
analysisStatus = logger.startProgress('Analyzing $analysisTarget...', timeout: kSlowOperation);
analyzedPaths.clear();
analysisTimer = Stopwatch()..start();
} else {
analysisStatus?.stop();
analysisStatus = null;
analysisTimer.stop();
logger.printStatus(terminal.clearScreen(), newline: false);
......
......@@ -107,7 +107,7 @@ class AnalyzeOnce extends AnalyzeBase {
? '${directories.length} ${directories.length == 1 ? 'directory' : 'directories'}'
: fs.path.basename(directories.first);
final Status progress = argResults['preamble']
? logger.startProgress('Analyzing $message...')
? logger.startProgress('Analyzing $message...', timeout: kSlowOperation)
: null;
await analysisCompleter.future;
......
......@@ -149,7 +149,7 @@ class AttachCommand extends FlutterCommand {
}
final Status status = logger.startProgress(
'Waiting for a connection from Flutter on ${device.name}...',
expectSlowOperation: true,
timeout: kSlowOperation,
);
try {
final int localPort = await device.findIsolatePort(module, localPorts);
......@@ -179,7 +179,7 @@ class AttachCommand extends FlutterCommand {
observatoryUri = await observatoryDiscovery.uri;
// Determine ipv6 status from the scanned logs.
usesIpv6 = observatoryDiscovery.ipv6;
printStatus('Done.');
printStatus('Done.'); // FYI, this message is used as a sentinel in tests.
} finally {
await observatoryDiscovery?.cancel();
}
......@@ -217,20 +217,29 @@ class AttachCommand extends FlutterCommand {
flutterDevice.startEchoingDeviceLog();
}
int result;
if (daemon != null) {
AppInstance app;
try {
app = await daemon.appDomain.launch(hotRunner, hotRunner.attach,
device, null, true, fs.currentDirectory);
app = await daemon.appDomain.launch(
hotRunner,
hotRunner.attach,
device,
null,
true,
fs.currentDirectory,
);
} catch (error) {
throwToolExit(error.toString());
}
final int result = await app.runner.waitForAppToFinish();
if (result != 0)
throwToolExit(null, exitCode: result);
result = await app.runner.waitForAppToFinish();
assert(result != null);
} else {
await hotRunner.attach();
result = await hotRunner.attach();
assert(result != null);
}
if (result != 0)
throwToolExit(null, exitCode: result);
} finally {
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
for (ForwardedPort port in ports) {
......
......@@ -71,7 +71,7 @@ class BuildAotCommand extends BuildSubCommand {
final String typeName = artifacts.getEngineType(platform, buildMode);
status = logger.startProgress(
'Building AOT snapshot in ${getModeName(getBuildMode())} mode ($typeName)...',
expectSlowOperation: true,
timeout: kSlowOperation,
);
}
final String outputPath = argResults['output-dir'] ?? getAotBuildDirectory();
......
......@@ -428,11 +428,12 @@ class AppDomain extends Domain {
});
}
final Completer<void> appStartedCompleter = Completer<void>();
// We don't want to wait for this future to complete and callbacks won't fail.
// As it just writes to stdout.
appStartedCompleter.future.then<void>((_) { // ignore: unawaited_futures
_sendAppEvent(app, 'started');
});
// We don't want to wait for this future to complete and callbacks won't fail,
// as it just writes to stdout.
appStartedCompleter.future // ignore: unawaited_futures
.then<void>((void value) {
_sendAppEvent(app, 'started');
});
await app._runInZone<void>(this, () async {
try {
......@@ -515,14 +516,15 @@ class AppDomain extends Domain {
if (app == null)
throw "app '$appId' not found";
return app.stop().timeout(const Duration(seconds: 5)).then<bool>((_) {
return true;
}).catchError((dynamic error) {
_sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
app.closeLogger();
_apps.remove(app);
return false;
});
return app.stop().then<bool>(
(void value) => true,
onError: (dynamic error, StackTrace stack) {
_sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
app.closeLogger();
_apps.remove(app);
return false;
},
);
}
Future<bool> detach(Map<String, dynamic> args) async {
......@@ -532,14 +534,15 @@ class AppDomain extends Domain {
if (app == null)
throw "app '$appId' not found";
return app.detach().timeout(const Duration(seconds: 5)).then<bool>((_) {
return true;
}).catchError((dynamic error) {
_sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
app.closeLogger();
_apps.remove(app);
return false;
});
return app.detach().then<bool>(
(void value) => true,
onError: (dynamic error, StackTrace stack) {
_sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
app.closeLogger();
_apps.remove(app);
return false;
},
);
}
AppInstance _getApp(String id) {
......@@ -772,13 +775,14 @@ class NotifyingLogger extends Logger {
@override
Status startProgress(
String message, {
@required Duration timeout,
String progressId,
bool expectSlowOperation = false,
bool multilineOutput,
int progressIndicatorPadding = kDefaultStatusPadding,
}) {
assert(timeout != null);
printStatus(message);
return Status();
return SilentStatus(timeout: timeout);
}
void dispose() {
......@@ -948,11 +952,12 @@ class _AppRunLogger extends Logger {
@override
Status startProgress(
String message, {
@required Duration timeout,
String progressId,
bool expectSlowOperation = false,
bool multilineOutput,
int progressIndicatorPadding = 52,
}) {
assert(timeout != null);
final int id = _nextProgressId++;
_sendProgressEvent(<String, dynamic>{
......@@ -961,13 +966,16 @@ class _AppRunLogger extends Logger {
'message': message,
});
_status = Status(onFinish: () {
_status = null;
_sendProgressEvent(<String, dynamic>{
'id': id.toString(),
'progressId': progressId,
'finished': true
});
_status = SilentStatus(
timeout: timeout,
onFinish: () {
_status = null;
_sendProgressEvent(<String, dynamic>{
'id': id.toString(),
'progressId': progressId,
'finished': true,
},
);
})..start();
return _status;
}
......
......@@ -29,11 +29,11 @@ class LogsCommand extends FlutterCommand {
Device device;
@override
Future<FlutterCommandResult> verifyThenRunCommand() async {
Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
device = await findTargetDevice();
if (device == null)
throwToolExit(null);
return super.verifyThenRunCommand();
return super.verifyThenRunCommand(commandPath);
}
@override
......
......@@ -64,7 +64,7 @@ class ScreenshotCommand extends FlutterCommand {
Device device;
@override
Future<FlutterCommandResult> verifyThenRunCommand() async {
Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
device = await findTargetDevice();
if (device == null)
throwToolExit('Must have a connected device');
......@@ -72,7 +72,7 @@ class ScreenshotCommand extends FlutterCommand {
throwToolExit('Screenshot not supported for ${device.name}.');
if (argResults[_kType] != _kDeviceType && argResults[_kObservatoryPort] == null)
throwToolExit('Observatory port must be specified for screenshot type ${argResults[_kType]}');
return super.verifyThenRunCommand();
return super.verifyThenRunCommand(commandPath);
}
@override
......
......@@ -95,7 +95,7 @@ class UpdatePackagesCommand extends FlutterCommand {
Future<void> _downloadCoverageData() async {
final Status status = logger.startProgress(
'Downloading lcov data for package:flutter...',
expectSlowOperation: true,
timeout: kSlowOperation,
);
final String urlBase = platform.environment['FLUTTER_STORAGE_BASE_URL'] ?? 'https://storage.googleapis.com';
final List<int> data = await fetchUrl(Uri.parse('$urlBase/flutter_infra/flutter/coverage/lcov.info'));
......
......@@ -92,7 +92,7 @@ Future<void> pubGet({
final String command = upgrade ? 'upgrade' : 'get';
final Status status = logger.startProgress(
'Running "flutter packages $command" in ${fs.path.basename(directory)}...',
expectSlowOperation: true,
timeout: kSlowOperation,
);
final List<String> args = <String>['--verbosity=warning'];
if (FlutterCommand.current != null && FlutterCommand.current.globalResults['verbose'])
......
......@@ -150,7 +150,7 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery {
final List<Device> devices = await pollingGetDevices().timeout(_pollingTimeout);
_items.updateWithNewList(devices);
} on TimeoutException {
printTrace('Device poll timed out.');
printTrace('Device poll timed out. Will retry.');
}
}, _pollingInterval);
}
......
......@@ -184,7 +184,10 @@ class Doctor {
for (ValidatorTask validatorTask in startValidatorTasks()) {
final DoctorValidator validator = validatorTask.validator;
final Status status = Status.withSpinner();
final Status status = Status.withSpinner(
timeout: kFastOperation,
slowWarningCallback: () => validator.slowWarning,
);
ValidationResult result;
try {
result = await validatorTask.result;
......@@ -286,6 +289,8 @@ abstract class DoctorValidator {
final String title;
String get slowWarning => 'This is taking an unexpectedly long time...';
Future<ValidationResult> validate();
}
......@@ -298,6 +303,10 @@ class GroupedValidator extends DoctorValidator {
final List<DoctorValidator> subValidators;
@override
String get slowWarning => _currentSlowWarning;
String _currentSlowWarning = 'Initializing...';
@override
Future<ValidationResult> validate() async {
final List<ValidatorTask> tasks = <ValidatorTask>[];
......@@ -307,8 +316,10 @@ class GroupedValidator extends DoctorValidator {
final List<ValidationResult> results = <ValidationResult>[];
for (ValidatorTask subValidator in tasks) {
_currentSlowWarning = subValidator.validator.slowWarning;
results.add(await subValidator.result);
}
_currentSlowWarning = 'Merging results...';
return _mergeValidationResults(results);
}
......@@ -671,6 +682,9 @@ class IntelliJValidatorOnMac extends IntelliJValidator {
class DeviceValidator extends DoctorValidator {
DeviceValidator() : super('Connected device');
@override
String get slowWarning => 'Scanning for devices is taking a long time...';
@override
Future<ValidationResult> validate() async {
final List<Device> devices = await deviceManager.getAllConnectedDevices().toList();
......
......@@ -220,7 +220,7 @@ class CocoaPods {
}
Future<void> _runPodInstall(IosProject iosProject, String engineDirectory) async {
final Status status = logger.startProgress('Running pod install...', expectSlowOperation: true);
final Status status = logger.startProgress('Running pod install...', timeout: kSlowOperation);
final ProcessResult result = await processManager.run(
<String>['pod', 'install', '--verbose'],
workingDirectory: iosProject.hostAppRoot.path,
......
......@@ -26,8 +26,6 @@ const String _kIdeviceinstallerInstructions =
'To work with iOS devices, please install ideviceinstaller. To install, run:\n'
'brew install ideviceinstaller.';
const Duration kPortForwardTimeout = Duration(seconds: 10);
class IOSDeploy {
const IOSDeploy();
......@@ -297,7 +295,7 @@ class IOSDevice extends Device {
int installationResult = -1;
Uri localObservatoryUri;
final Status installStatus = logger.startProgress('Installing and launching...', expectSlowOperation: true);
final Status installStatus = logger.startProgress('Installing and launching...', timeout: kSlowOperation);
if (!debuggingOptions.debuggingEnabled) {
// If debugging is not enabled, just launch the application and continue.
......
......@@ -470,7 +470,7 @@ Future<XcodeBuildResult> buildXcodeProject({
initialBuildStatus.cancel();
buildSubStatus = logger.startProgress(
line,
expectSlowOperation: true,
timeout: kSlowOperation,
progressIndicatorPadding: kDefaultStatusPadding - 7,
);
}
......@@ -485,7 +485,7 @@ Future<XcodeBuildResult> buildXcodeProject({
}
final Stopwatch buildStopwatch = Stopwatch()..start();
initialBuildStatus = logger.startProgress('Starting Xcode build...');
initialBuildStatus = logger.startProgress('Starting Xcode build...', timeout: kFastOperation);
final RunResult buildResult = await runAsync(
buildCommands,
workingDirectory: app.project.hostAppRoot.path,
......
......@@ -9,7 +9,6 @@ import 'package:meta/meta.dart';
import 'application_package.dart';
import 'artifacts.dart';
import 'asset.dart';
import 'base/common.dart';
import 'base/file_system.dart';
import 'base/io.dart';
import 'base/logger.dart';
......@@ -72,11 +71,13 @@ class FlutterDevice {
if (vmServices != null)
return;
final List<VMService> localVmServices = List<VMService>(observatoryUris.length);
for (int i = 0; i < observatoryUris.length; i++) {
for (int i = 0; i < observatoryUris.length; i += 1) {
printTrace('Connecting to service protocol: ${observatoryUris[i]}');
localVmServices[i] = await VMService.connect(observatoryUris[i],
reloadSources: reloadSources,
compileExpression: compileExpression);
localVmServices[i] = await VMService.connect(
observatoryUris[i],
reloadSources: reloadSources,
compileExpression: compileExpression,
);
printTrace('Successfully connected to service protocol: ${observatoryUris[i]}');
}
vmServices = localVmServices;
......@@ -112,13 +113,16 @@ class FlutterDevice {
final List<FlutterView> flutterViews = views;
if (flutterViews == null || flutterViews.isEmpty)
return;
final List<Future<void>> futures = <Future<void>>[];
for (FlutterView view in flutterViews) {
if (view != null && view.uiIsolate != null) {
// Manage waits specifically below.
view.uiIsolate.flutterExit(); // ignore: unawaited_futures
futures.add(view.uiIsolate.flutterExit());
}
}
await Future<void>.delayed(const Duration(milliseconds: 100));
// The flutterExit message only returns if it fails, so just wait a few
// seconds then assume it worked.
// TODO(ianh): We should make this return once the VM service disconnects.
await Future.wait(futures).timeout(const Duration(seconds: 2), onTimeout: () { });
}
Future<Uri> setupDevFS(String fsName,
......@@ -382,7 +386,7 @@ class FlutterDevice {
}) async {
final Status devFSStatus = logger.startProgress(
'Syncing files to device ${device.name}...',
expectSlowOperation: true,
timeout: kFastOperation,
);
int bytes = 0;
try {
......@@ -474,11 +478,14 @@ abstract class ResidentRunner {
}
/// Start the app and keep the process running during its lifetime.
///
/// Returns the exit code that we should use for the flutter tool process; 0
/// for success, 1 for user error (e.g. bad arguments), 2 for other failures.
Future<int> run({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
String route,
bool shouldBuild = true
bool shouldBuild = true,
});
bool get supportsRestart => false;
......@@ -493,7 +500,7 @@ abstract class ResidentRunner {
await _debugSaveCompilationTrace();
await stopEchoingDeviceLog();
await preStop();
return stopApp();
await stopApp();
}
Future<void> detach() async {
......@@ -558,7 +565,7 @@ abstract class ResidentRunner {
}
Future<void> _screenshot(FlutterDevice device) async {
final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...');
final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...', timeout: kFastOperation);
final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png');
try {
if (supportsServiceProtocol && isRunningDebug) {
......@@ -673,24 +680,32 @@ abstract class ResidentRunner {
}
/// If the [reloadSources] parameter is not null the 'reloadSources' service
/// will be registered
/// will be registered.
//
// Failures should be indicated by completing the future with an error, using
// a string as the error object, which will be used by the caller (attach())
// to display an error message.
Future<void> connectToServiceProtocol({ReloadSources reloadSources, CompileExpression compileExpression}) async {
if (!debuggingOptions.debuggingEnabled)
return Future<void>.error('Error the service protocol is not enabled.');
throw 'The service protocol is not enabled.';
bool viewFound = false;
for (FlutterDevice device in flutterDevices) {
await device._connect(reloadSources: reloadSources,
compileExpression: compileExpression);
await device._connect(
reloadSources: reloadSources,
compileExpression: compileExpression,
);
await device.getVMs();
await device.refreshViews();
if (device.views.isEmpty)
printStatus('No Flutter views available on ${device.device.name}');
else
if (device.views.isNotEmpty)
viewFound = true;
}
if (!viewFound)
throwToolExit('No Flutter view is available');
if (!viewFound) {
if (flutterDevices.length == 1)
throw 'No Flutter view is available on ${flutterDevices.first.device.name}.';
throw 'No Flutter view is available on any device '
'(${flutterDevices.map<String>((FlutterDevice device) => device.device.name).join(', ')}).';
}
// Listen for service protocol connection to close.
for (FlutterDevice device in flutterDevices) {
......@@ -841,12 +856,13 @@ abstract class ResidentRunner {
printHelp(details: false);
}
terminal.singleCharMode = true;
terminal.onCharInput.listen(processTerminalInput);
terminal.keystrokes.listen(processTerminalInput);
}
}
Future<int> waitForAppToFinish() async {
final int exitCode = await _finished.future;
assert(exitCode != null);
await cleanupAtFinish();
return exitCode;
}
......@@ -867,8 +883,10 @@ abstract class ResidentRunner {
Future<void> preStop() async { }
Future<void> stopApp() async {
final List<Future<void>> futures = <Future<void>>[];
for (FlutterDevice device in flutterDevices)
await device.stopApps();
futures.add(device.stopApps());
await Future.wait(futures);
appFinished();
}
......
......@@ -64,8 +64,14 @@ class ColdRunner extends ResidentRunner {
}
// Connect to observatory.
if (debuggingOptions.debuggingEnabled)
await connectToServiceProtocol();
if (debuggingOptions.debuggingEnabled) {
try {
await connectToServiceProtocol();
} on String catch (message) {
printError(message);
return 2;
}
}
if (flutterDevices.first.observatoryUris != null) {
// For now, only support one debugger connection.
......
This diff is collapsed.
......@@ -454,19 +454,17 @@ abstract class FlutterCommand extends Command<void> {
body: () async {
if (flutterUsage.isFirstRun)
flutterUsage.printWelcome();
final String commandPath = await usagePath;
FlutterCommandResult commandResult;
try {
commandResult = await verifyThenRunCommand();
commandResult = await verifyThenRunCommand(commandPath);
} on ToolExit {
commandResult = const FlutterCommandResult(ExitStatus.fail);
rethrow;
} finally {
final DateTime endTime = systemClock.now();
printTrace('"flutter $name" took ${getElapsedAsMilliseconds(endTime.difference(startTime))}.');
// This is checking the result of the call to 'usagePath'
// (a Future<String>), and not the result of evaluating the Future.
if (usagePath != null) {
if (commandPath != null) {
final List<String> labels = <String>[];
if (commandResult?.exitStatus != null)
labels.add(getEnumName(commandResult.exitStatus));
......@@ -500,7 +498,7 @@ abstract class FlutterCommand extends Command<void> {
/// then call this method to execute the command
/// rather than calling [runCommand] directly.
@mustCallSuper
Future<FlutterCommandResult> verifyThenRunCommand() async {
Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
await validateCommand();
// Populate the cache. We call this before pub get below so that the sky_engine
......@@ -516,8 +514,6 @@ abstract class FlutterCommand extends Command<void> {
setupApplicationPackages();
final String commandPath = await usagePath;
if (commandPath != null) {
final Map<String, String> additionalUsageValues = await usageValues;
flutterUsage.sendCommand(commandPath, parameters: additionalUsageValues);
......
......@@ -57,13 +57,7 @@ class CoverageCollector extends TestWatcher {
if (result == null)
throw Exception('Failed to collect coverage.');
data = result;
})
.timeout(
const Duration(minutes: 10),
onTimeout: () {
throw Exception('Timed out while collecting coverage.');
},
);
});
await Future.any<void>(<Future<void>>[ processComplete, collectionComplete ]);
assert(data != null);
......@@ -77,12 +71,8 @@ class CoverageCollector extends TestWatcher {
///
/// This will not start any collection tasks. It us up to the caller of to
/// call [collectCoverage] for each process first.
///
/// If [timeout] is specified, the future will timeout (with a
/// [TimeoutException]) after the specified duration.
Future<String> finalizeCoverage({
coverage.Formatter formatter,
Duration timeout,
Directory coverageDirectory,
}) async {
printTrace('formating coverage data');
......@@ -102,9 +92,8 @@ class CoverageCollector extends TestWatcher {
}
Future<bool> collectCoverageData(String coveragePath, { bool mergeCoverageData = false, Directory coverageDirectory }) async {
final Status status = logger.startProgress('Collecting coverage information...');
final Status status = logger.startProgress('Collecting coverage information...', timeout: kFastOperation);
final String coverageData = await finalizeCoverage(
timeout: const Duration(seconds: 30),
coverageDirectory: coverageDirectory,
);
status.stop();
......
......@@ -33,11 +33,17 @@ import 'watcher.dart';
/// The timeout we give the test process to connect to the test harness
/// once the process has entered its main method.
///
/// We time out test execution because we expect some tests to hang and we want
/// to know which test hung, rather than have the entire test harness just do
/// nothing for a few hours until the user (or CI environment) gets bored.
const Duration _kTestStartupTimeout = Duration(minutes: 1);
/// The timeout we give the test process to start executing Dart code. When the
/// CPU is under severe load, this can take a while, but it's not indicative of
/// any problem with Flutter, so we give it a large timeout.
///
/// See comment under [_kTestStartupTimeout] regarding timeouts.
const Duration _kTestProcessTimeout = Duration(minutes: 5);
/// Message logged by the test process to signal that its main method has begun
......@@ -288,13 +294,11 @@ class _Compiler {
firstCompile = true;
}
suppressOutput = false;
final CompilerOutput compilerOutput = await handleTimeout<CompilerOutput>(
compiler.recompile(
request.path,
<String>[request.path],
outputPath: outputDill.path),
request.path,
);
final CompilerOutput compilerOutput = await compiler.recompile(
request.path,
<String>[request.path],
outputPath: outputDill.path,
);
final String outputPath = compilerOutput?.outputFilename;
// In case compiler didn't produce output or reported compilation
......@@ -337,7 +341,7 @@ class _Compiler {
Future<String> compile(String mainDart) {
final Completer<String> completer = Completer<String>();
compilerController.add(_CompilationRequest(mainDart, completer));
return handleTimeout<String>(completer.future, mainDart);
return completer.future;
}
Future<void> _shutdown() async {
......@@ -353,13 +357,6 @@ class _Compiler {
await _shutdown();
await compilerController.close();
}
static Future<T> handleTimeout<T>(Future<T> value, String path) {
return value.timeout(const Duration(minutes: 5), onTimeout: () {
printError('Compilation of $path timed out after 5 minutes.');
return null;
});
}
}
class _FlutterPlatform extends PlatformPlugin {
......
......@@ -35,14 +35,12 @@ class Tracing {
bool waitForFirstFrame = false
}) async {
Map<String, dynamic> timeline;
if (!waitForFirstFrame) {
// Stop tracing immediately and get the timeline
await vmService.vm.setVMTimelineFlags(<String>[]);
timeline = await vmService.vm.getVMTimeline();
} else {
final Completer<void> whenFirstFrameRendered = Completer<void>();
(await vmService.onTimelineEvent).listen((ServiceEvent timelineEvent) {
final List<Map<String, dynamic>> events = timelineEvent.timelineEvents;
for (Map<String, dynamic> event in events) {
......@@ -50,24 +48,10 @@ class Tracing {
whenFirstFrameRendered.complete();
}
});
await whenFirstFrameRendered.future.timeout(
const Duration(seconds: 10),
onTimeout: () {
printError(
'Timed out waiting for the first frame event. Either the '
'application failed to start, or the event was missed because '
'"flutter run" took too long to subscribe to timeline events.'
);
return null;
}
);
await whenFirstFrameRendered.future;
timeline = await vmService.vm.getVMTimeline();
await vmService.vm.setVMTimelineFlags(<String>[]);
}
return timeline;
}
}
......
......@@ -14,7 +14,6 @@ class VsCodeValidator extends DoctorValidator {
final VsCode _vsCode;
static Iterable<DoctorValidator> get installedValidators {
return VsCode
.allInstalled()
......
......@@ -158,7 +158,7 @@ void main() {
fallbacks: <Type, Generator>{
int: () => int.parse(context[String]),
String: () => '${context[double]}',
double: () => context[int] * 1.0,
double: () => (context[int] as int) * 1.0, // ignore: avoid_as
},
);
try {
......
......@@ -165,7 +165,7 @@ Stream<String> mockStdInStream;
class TestTerminal extends AnsiTerminal {
@override
Stream<String> get onCharInput {
Stream<String> get keystrokes {
return mockStdInStream;
}
}
......@@ -90,5 +90,5 @@ void main() {
expect(result, isList);
expect(result, isNotEmpty);
});
}, timeout: const Timeout.factor(2));
}, timeout: const Timeout.factor(10)); // This test uses the `flutter` tool, which could be blocked behind the startup lock for a long time.
}
......@@ -28,13 +28,15 @@ void main() {
});
test('can step over statements', () async {
await _flutter.run(withDebugger: true);
await _flutter.run(withDebugger: true, startPaused: true);
await _flutter.addBreakpoint(_project.breakpointUri, _project.breakpointLine);
await _flutter.resume();
await _flutter.waitForPause(); // Now we should be on the breakpoint.
// Stop at the initial breakpoint that the expected steps are based on.
await _flutter.breakAt(_project.breakpointUri, _project.breakpointLine, restart: true);
expect((await _flutter.getSourceLocation()).line, equals(_project.breakpointLine));
// Issue 5 steps, ensuring that we end up on the annotated lines each time.
for (int i = 1; i <= _project.numberOfSteps; i++) {
for (int i = 1; i <= _project.numberOfSteps; i += 1) {
await _flutter.stepOverOrOverAsyncSuspension();
final SourcePosition location = await _flutter.getSourceLocation();
final int actualLine = location.line;
......@@ -47,5 +49,5 @@ void main() {
reason: 'After $i steps, debugger should stop at $expectedLine but stopped at $actualLine');
}
});
}, timeout: const Timeout.factor(3));
}, timeout: const Timeout.factor(10)); // The DevFS sync takes a really long time, so these tests can be slow.
}
......@@ -31,16 +31,18 @@ void main() {
tryToDelete(tempDir);
});
Future<Isolate> breakInBuildMethod(FlutterTestDriver flutter) async {
return _flutter.breakAt(
_project.buildMethodBreakpointUri,
_project.buildMethodBreakpointLine);
Future<void> breakInBuildMethod(FlutterTestDriver flutter) async {
await _flutter.breakAt(
_project.buildMethodBreakpointUri,
_project.buildMethodBreakpointLine,
);
}
Future<Isolate> breakInTopLevelFunction(FlutterTestDriver flutter) async {
return _flutter.breakAt(
_project.topLevelFunctionBreakpointUri,
_project.topLevelFunctionBreakpointLine);
Future<void> breakInTopLevelFunction(FlutterTestDriver flutter) async {
await _flutter.breakAt(
_project.topLevelFunctionBreakpointUri,
_project.topLevelFunctionBreakpointLine,
);
}
test('can evaluate trivial expressions in top level function', () async {
......@@ -78,7 +80,8 @@ void main() {
await breakInBuildMethod(_flutter);
await evaluateComplexReturningExpressions(_flutter);
});
}, timeout: const Timeout.factor(6));
}, timeout: const Timeout.factor(10)); // The DevFS sync takes a really long time, so these tests can be slow.
}
Future<void> evaluateTrivialExpressions(FlutterTestDriver flutter) async {
......
......@@ -18,8 +18,8 @@ void main() {
setUp(() async {
tempDir = createResolvedTempDirectorySync();
await _project.setUpIn(tempDir);
_flutterRun = FlutterRunTestDriver(tempDir, logPrefix: 'RUN');
_flutterAttach = FlutterRunTestDriver(tempDir, logPrefix: 'ATTACH');
_flutterRun = FlutterRunTestDriver(tempDir, logPrefix: ' RUN ');
_flutterAttach = FlutterRunTestDriver(tempDir, logPrefix: 'ATTACH ');
});
tearDown(() async {
......@@ -58,5 +58,5 @@ void main() {
await _flutterAttach.attach(_flutterRun.vmServicePort);
await _flutterAttach.hotReload();
});
}, timeout: const Timeout.factor(6));
}, timeout: const Timeout.factor(10)); // The DevFS sync takes a really long time, so these tests can be slow.
}
......@@ -55,5 +55,5 @@ void main() {
await _flutter.run(pidFile: pidFile);
expect(pidFile.existsSync(), isTrue);
});
}, timeout: const Timeout.factor(6));
}, timeout: const Timeout.factor(10)); // The DevFS sync takes a really long time, so these tests can be slow.
}
......@@ -14,7 +14,7 @@ import 'test_driver.dart';
import 'test_utils.dart';
void main() {
group('hot', () {
group('hot reload tests', () {
Directory tempDir;
final HotReloadProject _project = HotReloadProject();
FlutterRunTestDriver _flutter;
......@@ -26,39 +26,127 @@ void main() {
});
tearDown(() async {
await _flutter.stop();
await _flutter?.stop();
tryToDelete(tempDir);
});
test('reload works without error', () async {
test('hot reload works without error', () async {
await _flutter.run();
await _flutter.hotReload();
});
test('newly added code executes during reload', () async {
test('newly added code executes during hot reload', () async {
await _flutter.run();
_project.uncommentHotReloadPrint();
final StringBuffer stdout = StringBuffer();
final StreamSubscription<String> sub = _flutter.stdout.listen(stdout.writeln);
final StreamSubscription<String> subscription = _flutter.stdout.listen(stdout.writeln);
try {
await _flutter.hotReload();
expect(stdout.toString(), contains('(((((RELOAD WORKED)))))'));
} finally {
await sub.cancel();
await subscription.cancel();
}
});
test('restart works without error', () async {
test('hot restart works without error', () async {
await _flutter.run();
await _flutter.hotRestart();
});
test('reload hits breakpoints after reload', () async {
test('breakpoints are hit after hot reload', () async {
Isolate isolate;
await _flutter.run(withDebugger: true, startPaused: true);
final Completer<void> sawTick1 = Completer<void>();
final Completer<void> sawTick3 = Completer<void>();
final Completer<void> sawDebuggerPausedMessage = Completer<void>();
final StreamSubscription<String> subscription = _flutter.stdout.listen(
(String line) {
if (line.contains('((((TICK 1))))')) {
expect(sawTick1.isCompleted, isFalse);
sawTick1.complete();
}
if (line.contains('((((TICK 3))))')) {
expect(sawTick3.isCompleted, isFalse);
sawTick3.complete();
}
if (line.contains('The application is paused in the debugger on a breakpoint.')) {
expect(sawDebuggerPausedMessage.isCompleted, isFalse);
sawDebuggerPausedMessage.complete();
}
},
);
await _flutter.resume(); // we start paused so we can set up our TICK 1 listener before the app starts
sawTick1.future.timeout( // ignore: unawaited_futures
const Duration(seconds: 5),
onTimeout: () { print('The test app is taking longer than expected to print its synchronization line...'); },
);
await sawTick1.future; // after this, app is in steady state
await _flutter.addBreakpoint(
_project.scheduledBreakpointUri,
_project.scheduledBreakpointLine,
);
await _flutter.hotReload(); // reload triggers code which eventually hits the breakpoint
isolate = await _flutter.waitForPause();
expect(isolate.pauseEvent.kind, equals(EventKind.kPauseBreakpoint));
await _flutter.resume();
await _flutter.addBreakpoint(
_project.buildBreakpointUri,
_project.buildBreakpointLine,
);
bool reloaded = false;
final Future<void> reloadFuture = _flutter.hotReload().then((void value) { reloaded = true; });
await sawTick3.future; // this should happen before it pauses
isolate = await _flutter.waitForPause();
expect(isolate.pauseEvent.kind, equals(EventKind.kPauseBreakpoint));
await sawDebuggerPausedMessage.future;
expect(reloaded, isFalse);
await _flutter.resume();
await reloadFuture;
expect(reloaded, isTrue);
reloaded = false;
await subscription.cancel();
});
test('hot reload doesn\'t reassemble if paused', () async {
await _flutter.run(withDebugger: true);
final Isolate isolate = await _flutter.breakAt(
_project.breakpointUri,
_project.breakpointLine);
final Completer<void> sawTick2 = Completer<void>();
final Completer<void> sawTick3 = Completer<void>();
final Completer<void> sawDebuggerPausedMessage1 = Completer<void>();
final Completer<void> sawDebuggerPausedMessage2 = Completer<void>();
final StreamSubscription<String> subscription = _flutter.stdout.listen(
(String line) {
if (line.contains('((((TICK 2))))')) {
expect(sawTick2.isCompleted, isFalse);
sawTick2.complete();
}
if (line.contains('The application is paused in the debugger on a breakpoint.')) {
expect(sawDebuggerPausedMessage1.isCompleted, isFalse);
sawDebuggerPausedMessage1.complete();
}
if (line.contains('The application is paused in the debugger on a breakpoint; interface might not update.')) {
expect(sawDebuggerPausedMessage2.isCompleted, isFalse);
sawDebuggerPausedMessage2.complete();
}
},
);
await _flutter.addBreakpoint(
_project.buildBreakpointUri,
_project.buildBreakpointLine,
);
bool reloaded = false;
final Future<void> reloadFuture = _flutter.hotReload().then((void value) { reloaded = true; });
await sawTick2.future; // this should happen before it pauses
final Isolate isolate = await _flutter.waitForPause();
expect(isolate.pauseEvent.kind, equals(EventKind.kPauseBreakpoint));
expect(reloaded, isFalse);
await sawDebuggerPausedMessage1.future; // this is the one where it say "uh, you broke into the debugger while reloading"
await reloadFuture; // this is the one where it times out because you're in the debugger
expect(reloaded, isTrue);
await _flutter.hotReload(); // now we're already paused
expect(sawTick3.isCompleted, isFalse);
await sawDebuggerPausedMessage2.future; // so we just get told that nothing is going to happen
await _flutter.resume();
await subscription.cancel();
});
}, timeout: const Timeout.factor(6));
}, timeout: const Timeout.factor(10)); // The DevFS sync takes a really long time, so these tests can be slow.
}
......@@ -45,5 +45,5 @@ void main() {
await Future<void>.delayed(requiredLifespan);
expect(_flutter.hasExited, equals(false));
});
}, timeout: const Timeout.factor(6));
}, timeout: const Timeout.factor(10)); // The DevFS sync takes a really long time, so these tests can be slow.
}
......@@ -19,15 +19,22 @@ class BasicProject extends Project {
@override
final String main = r'''
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
Future<void> main() async {
while (true) {
runApp(new MyApp());
await Future.delayed(const Duration(milliseconds: 50));
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
topLevelFunction();
return new MaterialApp( // BREAKPOINT
return new MaterialApp( // BUILD BREAKPOINT
title: 'Flutter Demo',
home: new Container(),
);
......@@ -39,9 +46,9 @@ class BasicProject extends Project {
}
''';
Uri get buildMethodBreakpointUri => breakpointUri;
int get buildMethodBreakpointLine => breakpointLine;
Uri get buildMethodBreakpointUri => mainDart;
int get buildMethodBreakpointLine => lineContaining(main, '// BUILD BREAKPOINT');
Uri get topLevelFunctionBreakpointUri => breakpointUri;
Uri get topLevelFunctionBreakpointUri => mainDart;
int get topLevelFunctionBreakpointLine => lineContaining(main, '// TOP LEVEL BREAKPOINT');
}
......@@ -22,33 +22,63 @@ class HotReloadProject extends Project {
@override
final String main = r'''
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() => runApp(new MyApp());
int count = 1;
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Do not remove this line, it's uncommented by a test to verify that hot
// reloading worked.
// This method gets called each time we hot reload, during reassemble.
// Do not remove the next line, it's uncommented by a test to verify that
// hot reloading worked:
// printHotReloadWorked();
return new MaterialApp( // BREAKPOINT
print('((((TICK $count))))');
// tick 1 = startup warmup frame
// tick 2 = hot reload warmup reassemble frame
// after that there's a post-hot-reload frame scheduled by the tool that
// doesn't trigger this to rebuild, but does trigger the first callback
// below, then that callback schedules another frame on which we do the
// breakpoint.
// tick 3 = second hot reload warmup reassemble frame (pre breakpoint)
if (count == 2) {
SchedulerBinding.instance.scheduleFrameCallback((Duration timestamp) {
SchedulerBinding.instance.scheduleFrameCallback((Duration timestamp) {
print('breakpoint line'); // SCHEDULED BREAKPOINT
});
});
}
count += 1;
return MaterialApp( // BUILD BREAKPOINT
title: 'Flutter Demo',
home: new Container(),
home: Container(),
);
}
}
printHotReloadWorked() {
void printHotReloadWorked() {
// The call to this function is uncommented by a test to verify that hot
// reloading worked.
print('(((((RELOAD WORKED)))))');
}
''';
Uri get scheduledBreakpointUri => mainDart;
int get scheduledBreakpointLine => lineContaining(main, '// SCHEDULED BREAKPOINT');
Uri get buildBreakpointUri => mainDart;
int get buildBreakpointLine => lineContaining(main, '// BUILD BREAKPOINT');
void uncommentHotReloadPrint() {
final String newMainContents = main.replaceAll(
'// printHotReloadWorked();', 'printHotReloadWorked();');
'// printHotReloadWorked();',
'printHotReloadWorked();'
);
writeFile(fs.path.join(dir.path, 'lib', 'main.dart'), newMainContents);
}
}
......@@ -15,9 +15,7 @@ abstract class Project {
String get pubspec;
String get main;
// Valid locations for a breakpoint for tests that just need to break somewhere.
Uri get breakpointUri => Uri.parse('package:test/main.dart');
int get breakpointLine => lineContaining(main, '// BREAKPOINT');
Uri get mainDart => Uri.parse('package:test/main.dart');
Future<void> setUpIn(Directory dir) async {
this.dir = dir;
......@@ -32,6 +30,6 @@ abstract class Project {
final int index = contents.split('\n').indexWhere((String l) => l.contains(search));
if (index == -1)
throw Exception("Did not find '$search' inside the file");
return index;
return index + 1; // first line is line 1, not line 0
}
}
......@@ -35,11 +35,11 @@ class SteppingProject extends Project {
Future<void> doAsyncStuff() async {
print("test"); // BREAKPOINT
await new Future.value(true); // STEP 1
await new Future.microtask(() => true); // STEP 2 // STEP 3
await new Future.delayed(const Duration(milliseconds: 1)); // STEP 4 // STEP 5
print("done!"); // STEP 6
}
await new Future.value(true); // STEP 1 // STEP 2
await new Future.microtask(() => true); // STEP 3 // STEP 4
await new Future.delayed(const Duration(milliseconds: 1)); // STEP 5 // STEP 6
print("done!"); // STEP 7
} // STEP 8
@override
Widget build(BuildContext context) {
......@@ -51,7 +51,9 @@ class SteppingProject extends Project {
}
''';
Uri get breakpointUri => mainDart;
int get breakpointLine => lineContaining(main, '// BREAKPOINT');
int lineForStep(int i) => lineContaining(main, '// STEP $i');
final int numberOfSteps = 6;
final int numberOfSteps = 8;
}
......@@ -470,7 +470,7 @@ class TestTerminal extends AnsiTerminal {
String bolden(String message) => '<bold>$message</bold>';
@override
Stream<String> get onCharInput {
Stream<String> get keystrokes {
return mockTerminalStdInStream;
}
}
......@@ -153,13 +153,32 @@ class MockPeer implements rpc.Peer {
}
void main() {
final MockStdio mockStdio = MockStdio();
MockStdio mockStdio;
group('VMService', () {
setUp(() {
mockStdio = MockStdio();
});
testUsingContext('fails connection eagerly in the connect() method', () async {
expect(
VMService.connect(Uri.parse('http://host.invalid:9999/')),
throwsToolExit(),
);
FakeAsync().run((FakeAsync time) {
bool failed = false;
final Future<VMService> future = VMService.connect(Uri.parse('http://host.invalid:9999/'));
future.whenComplete(() {
failed = true;
});
time.elapse(const Duration(seconds: 5));
expect(failed, isFalse);
expect(mockStdio.writtenToStdout.join(''), '');
expect(mockStdio.writtenToStderr.join(''), '');
time.elapse(const Duration(seconds: 5));
expect(failed, isFalse);
expect(mockStdio.writtenToStdout.join(''), 'This is taking longer than expected...\n');
expect(mockStdio.writtenToStderr.join(''), '');
});
}, overrides: <Type, Generator>{
Logger: () => StdoutLogger(),
Stdio: () => mockStdio,
WebSocketConnector: () => (String url) async => throw const SocketException('test'),
});
testUsingContext('refreshViews', () {
......@@ -167,7 +186,7 @@ void main() {
bool done = false;
final MockPeer mockPeer = MockPeer();
expect(mockPeer.returnedFromSendRequest, 0);
final VMService vmService = VMService(mockPeer, null, null, const Duration(seconds: 1), null, null);
final VMService vmService = VMService(mockPeer, null, null, null, null);
vmService.getVM().then((void value) { done = true; });
expect(done, isFalse);
expect(mockPeer.returnedFromSendRequest, 0);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment