mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
[O] Removing all timeouts (mark II) (#26736)
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.
This commit is contained in:
parent
fa79c8137d
commit
31a9626c48
@ -119,7 +119,7 @@ Notably, it will start and stop gradle, for instance.
|
|||||||
To run all tests defined in `manifest.yaml`, use option `-a` (`--all`):
|
To run all tests defined in `manifest.yaml`, use option `-a` (`--all`):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
dart bin/run.dart -a
|
../../bin/cache/dart-sdk/bin/dart bin/run.dart -a
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running specific tests
|
## Running specific tests
|
||||||
@ -128,7 +128,7 @@ To run a test, use option `-t` (`--task`):
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
# from the .../flutter/dev/devicelab directory
|
# from the .../flutter/dev/devicelab directory
|
||||||
dart bin/run.dart -t {NAME_OR_PATH_OF_TEST}
|
../../bin/cache/dart-sdk/bin/dart bin/run.dart -t {NAME_OR_PATH_OF_TEST}
|
||||||
```
|
```
|
||||||
|
|
||||||
Where `NAME_OR_PATH_OF_TEST` can be either of:
|
Where `NAME_OR_PATH_OF_TEST` can be either of:
|
||||||
@ -142,7 +142,7 @@ Where `NAME_OR_PATH_OF_TEST` can be either of:
|
|||||||
To run multiple tests, repeat option `-t` (`--task`) multiple times:
|
To run multiple tests, repeat option `-t` (`--task`) multiple times:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
dart bin/run.dart -t test1 -t test2 -t test3
|
../../bin/cache/dart-sdk/bin/dart bin/run.dart -t test1 -t test2 -t test3
|
||||||
```
|
```
|
||||||
|
|
||||||
To run tests from a specific stage, use option `-s` (`--stage`).
|
To run tests from a specific stage, use option `-s` (`--stage`).
|
||||||
@ -151,7 +151,7 @@ Currently there are only three stages defined, `devicelab`,
|
|||||||
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
dart bin/run.dart -s {NAME_OF_STAGE}
|
../../bin/cache/dart-sdk/bin/dart bin/run.dart -s {NAME_OF_STAGE}
|
||||||
```
|
```
|
||||||
|
|
||||||
# Reproducing broken builds locally
|
# Reproducing broken builds locally
|
||||||
@ -162,7 +162,7 @@ failing test is `flutter_gallery__transition_perf`. This name can be passed to
|
|||||||
the `run.dart` command. For example:
|
the `run.dart` command. For example:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
dart bin/run.dart -t flutter_gallery__transition_perf
|
../../bin/cache/dart-sdk/bin/dart bin/run.dart -t flutter_gallery__transition_perf
|
||||||
```
|
```
|
||||||
|
|
||||||
# Writing tests
|
# Writing tests
|
||||||
|
@ -74,18 +74,18 @@ void main() {
|
|||||||
run.stdin.write('P');
|
run.stdin.write('P');
|
||||||
await driver.drive('none');
|
await driver.drive('none');
|
||||||
final Future<String> reloadStartingText =
|
final Future<String> reloadStartingText =
|
||||||
stdout.stream.firstWhere((String line) => line.endsWith('hot reload...'));
|
stdout.stream.firstWhere((String line) => line.endsWith('] Initializing hot reload...'));
|
||||||
final Future<String> reloadEndingText =
|
final Future<String> reloadEndingText =
|
||||||
stdout.stream.firstWhere((String line) => line.contains('Hot reload performed in '));
|
stdout.stream.firstWhere((String line) => line.contains('] Reloaded ') && line.endsWith('ms.'));
|
||||||
print('test: pressing "r" to perform a hot reload...');
|
print('test: pressing "r" to perform a hot reload...');
|
||||||
run.stdin.write('r');
|
run.stdin.write('r');
|
||||||
await reloadStartingText;
|
await reloadStartingText;
|
||||||
await reloadEndingText;
|
await reloadEndingText;
|
||||||
await driver.drive('none');
|
await driver.drive('none');
|
||||||
final Future<String> restartStartingText =
|
final Future<String> restartStartingText =
|
||||||
stdout.stream.firstWhere((String line) => line.endsWith('hot restart...'));
|
stdout.stream.firstWhere((String line) => line.endsWith('Performing hot restart...'));
|
||||||
final Future<String> restartEndingText =
|
final Future<String> restartEndingText =
|
||||||
stdout.stream.firstWhere((String line) => line.contains('Hot restart performed in '));
|
stdout.stream.firstWhere((String line) => line.contains('] Restarted application in '));
|
||||||
print('test: pressing "R" to perform a full reload...');
|
print('test: pressing "R" to perform a full reload...');
|
||||||
run.stdin.write('R');
|
run.stdin.write('R');
|
||||||
await restartStartingText;
|
await restartStartingText;
|
||||||
|
@ -29,7 +29,7 @@ Future<void> main() async {
|
|||||||
final SerializableFinder summary = find.byValueKey('summary');
|
final SerializableFinder summary = find.byValueKey('summary');
|
||||||
|
|
||||||
// Wait for calibration to complete and fab to appear.
|
// 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 String calibrationResult = await driver.getText(summary);
|
||||||
final Match matchCalibration = calibrationRegExp.matchAsPrefix(calibrationResult);
|
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(1)), closeTo(flutterFrameRate * 2.0, 5.0));
|
||||||
expect(double.parse(matchFast.group(2)), closeTo(flutterFrameRate, 10.0));
|
expect(double.parse(matchFast.group(2)), closeTo(flutterFrameRate, 10.0));
|
||||||
expect(int.parse(matchFast.group(3)), 1);
|
expect(int.parse(matchFast.group(3)), 1);
|
||||||
}, timeout: const Timeout(Duration(minutes: 1)));
|
});
|
||||||
|
|
||||||
tearDownAll(() async {
|
tearDownAll(() async {
|
||||||
driver?.close();
|
driver?.close();
|
||||||
|
@ -268,8 +268,7 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB
|
|||||||
void initServiceExtensions() {
|
void initServiceExtensions() {
|
||||||
super.initServiceExtensions();
|
super.initServiceExtensions();
|
||||||
|
|
||||||
const bool isReleaseMode = bool.fromEnvironment('dart.vm.product');
|
profile(() {
|
||||||
if (!isReleaseMode) {
|
|
||||||
registerSignalServiceExtension(
|
registerSignalServiceExtension(
|
||||||
name: 'debugDumpApp',
|
name: 'debugDumpApp',
|
||||||
callback: () {
|
callback: () {
|
||||||
@ -294,11 +293,14 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB
|
|||||||
name: 'didSendFirstFrameEvent',
|
name: 'didSendFirstFrameEvent',
|
||||||
callback: (_) async {
|
callback: (_) async {
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
|
// This is defined to return a STRING, not a boolean.
|
||||||
|
// Devtools, the Intellij plugin, and the flutter tool all depend
|
||||||
|
// on it returning a string and not a boolean.
|
||||||
'enabled': _needToReportFirstFrame ? 'false' : 'true'
|
'enabled': _needToReportFirstFrame ? 'false' : 'true'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
assert(() {
|
assert(() {
|
||||||
registerBoolServiceExtension(
|
registerBoolServiceExtension(
|
||||||
@ -540,22 +542,25 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB
|
|||||||
|
|
||||||
/// Whether the first frame has finished rendering.
|
/// Whether the first frame has finished rendering.
|
||||||
///
|
///
|
||||||
/// Only valid in profile and debug builds, it can't be used in release
|
/// Only useful in profile and debug builds; in release builds, this always
|
||||||
/// builds.
|
/// return false. This can be deferred using [deferFirstFrameReport] and
|
||||||
/// It can be deferred using [deferFirstFrameReport] and
|
/// [allowFirstFrameReport]. The value is set at the end of the call to
|
||||||
/// [allowFirstFrameReport].
|
/// [drawFrame].
|
||||||
/// The value is set at the end of the call to [drawFrame].
|
///
|
||||||
|
/// This value can also be obtained over the VM service protocol as
|
||||||
|
/// `ext.flutter.didSendFirstFrameEvent`.
|
||||||
bool get debugDidSendFirstFrameEvent => !_needToReportFirstFrame;
|
bool get debugDidSendFirstFrameEvent => !_needToReportFirstFrame;
|
||||||
|
|
||||||
/// Tell the framework not to report the frame it is building as a "useful"
|
/// Tell the framework not to report the frame it is building as a "useful"
|
||||||
/// first frame until there is a corresponding call to [allowFirstFrameReport].
|
/// first frame until there is a corresponding call to [allowFirstFrameReport].
|
||||||
///
|
///
|
||||||
/// This is used by [WidgetsApp] to report the first frame.
|
/// This is used by [WidgetsApp] to avoid reporting frames that aren't useful
|
||||||
//
|
/// during startup as the "first frame".
|
||||||
// TODO(ianh): This method should only be available in debug and profile modes.
|
|
||||||
void deferFirstFrameReport() {
|
void deferFirstFrameReport() {
|
||||||
|
profile(() {
|
||||||
assert(_deferFirstFrameReportCount >= 0);
|
assert(_deferFirstFrameReportCount >= 0);
|
||||||
_deferFirstFrameReportCount += 1;
|
_deferFirstFrameReportCount += 1;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// When called after [deferFirstFrameReport]: tell the framework to report
|
/// When called after [deferFirstFrameReport]: tell the framework to report
|
||||||
@ -564,12 +569,13 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB
|
|||||||
/// This method may only be called once for each corresponding call
|
/// This method may only be called once for each corresponding call
|
||||||
/// to [deferFirstFrameReport].
|
/// to [deferFirstFrameReport].
|
||||||
///
|
///
|
||||||
/// This is used by [WidgetsApp] to report the first frame.
|
/// This is used by [WidgetsApp] to report when the first useful frame is
|
||||||
//
|
/// painted.
|
||||||
// TODO(ianh): This method should only be available in debug and profile modes.
|
|
||||||
void allowFirstFrameReport() {
|
void allowFirstFrameReport() {
|
||||||
|
profile(() {
|
||||||
assert(_deferFirstFrameReportCount >= 1);
|
assert(_deferFirstFrameReportCount >= 1);
|
||||||
_deferFirstFrameReportCount -= 1;
|
_deferFirstFrameReportCount -= 1;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleBuildScheduled() {
|
void _handleBuildScheduled() {
|
||||||
@ -691,13 +697,13 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB
|
|||||||
return true;
|
return true;
|
||||||
}());
|
}());
|
||||||
}
|
}
|
||||||
// TODO(ianh): Following code should not be included in release mode, only profile and debug modes.
|
profile(() {
|
||||||
// See https://github.com/dart-lang/sdk/issues/27192
|
|
||||||
if (_needToReportFirstFrame && _reportFirstFrame) {
|
if (_needToReportFirstFrame && _reportFirstFrame) {
|
||||||
developer.Timeline.instantSync('Widgets completed first useful frame');
|
developer.Timeline.instantSync('Widgets completed first useful frame');
|
||||||
developer.postEvent('Flutter.FirstFrame', <String, dynamic>{});
|
developer.postEvent('Flutter.FirstFrame', <String, dynamic>{});
|
||||||
_needToReportFirstFrame = false;
|
_needToReportFirstFrame = false;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The [Element] that is at the root of the hierarchy (and which wraps the
|
/// The [Element] that is at the root of the hierarchy (and which wraps the
|
||||||
|
@ -9,16 +9,24 @@ import 'package:meta/meta.dart';
|
|||||||
abstract class Command {
|
abstract class Command {
|
||||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||||
/// const constructors so that they can be used in const expressions.
|
/// const constructors so that they can be used in const expressions.
|
||||||
const Command({ Duration timeout })
|
const Command({ this.timeout });
|
||||||
: timeout = timeout ?? const Duration(seconds: 5);
|
|
||||||
|
|
||||||
/// Deserializes this command from the value generated by [serialize].
|
/// Deserializes this command from the value generated by [serialize].
|
||||||
Command.deserialize(Map<String, String> json)
|
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.
|
/// 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;
|
final Duration timeout;
|
||||||
|
|
||||||
/// Identifies the type of the command object and of the handler.
|
/// Identifies the type of the command object and of the handler.
|
||||||
@ -26,10 +34,14 @@ abstract class Command {
|
|||||||
|
|
||||||
/// Serializes this command to parameter name/value pairs.
|
/// Serializes this command to parameter name/value pairs.
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
Map<String, String> serialize() => <String, String>{
|
Map<String, String> serialize() {
|
||||||
|
final Map<String, String> result = <String, String>{
|
||||||
'command': kind,
|
'command': kind,
|
||||||
'timeout': '${timeout.inMilliseconds}',
|
|
||||||
};
|
};
|
||||||
|
if (timeout != null)
|
||||||
|
result['timeout'] = '${timeout.inMilliseconds}';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An object sent from a Flutter application back to the Flutter Driver in
|
/// An object sent from a Flutter application back to the Flutter Driver in
|
||||||
|
@ -61,38 +61,13 @@ enum TimelineStream {
|
|||||||
|
|
||||||
const List<TimelineStream> _defaultStreams = <TimelineStream>[TimelineStream.all];
|
const List<TimelineStream> _defaultStreams = <TimelineStream>[TimelineStream.all];
|
||||||
|
|
||||||
/// Multiplies the timeout values used when establishing a connection to the
|
/// How long to wait before showing a message saying that
|
||||||
/// Flutter app under test and obtain an instance of [FlutterDriver].
|
/// things seem to be taking a long time.
|
||||||
///
|
const Duration _kUnusuallyLongTimeout = Duration(seconds: 5);
|
||||||
/// This multiplier applies automatically when using the default implementation
|
|
||||||
/// of the [vmServiceConnectFunction].
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
///
|
|
||||||
/// * [FlutterDriver.timeoutMultiplier], which multiplies all command timeouts by this number.
|
|
||||||
double connectionTimeoutMultiplier = _kDefaultTimeoutMultiplier;
|
|
||||||
|
|
||||||
const double _kDefaultTimeoutMultiplier = 1.0;
|
|
||||||
|
|
||||||
/// Default timeout for short-running RPCs.
|
|
||||||
Duration _shortTimeout(double multiplier) => const Duration(seconds: 5) * multiplier;
|
|
||||||
|
|
||||||
/// Default timeout for awaiting an Isolate to become runnable.
|
|
||||||
Duration _isolateLoadRunnableTimeout(double multiplier) => const Duration(minutes: 1) * multiplier;
|
|
||||||
|
|
||||||
/// Time to delay before driving a Fuchsia module.
|
|
||||||
Duration _fuchsiaDriveDelay(double multiplier) => const Duration(milliseconds: 500) * multiplier;
|
|
||||||
|
|
||||||
/// Default timeout for long-running RPCs.
|
|
||||||
Duration _longTimeout(double multiplier) => _shortTimeout(multiplier) * 6;
|
|
||||||
|
|
||||||
/// Additional amount of time we give the command to finish or timeout remotely
|
|
||||||
/// before timing out locally.
|
|
||||||
Duration _rpcGraceTime(double multiplier) => _shortTimeout(multiplier) ~/ 2;
|
|
||||||
|
|
||||||
/// The amount of time we wait prior to making the next attempt to connect to
|
/// The amount of time we wait prior to making the next attempt to connect to
|
||||||
/// the VM service.
|
/// the VM service.
|
||||||
Duration _pauseBetweenReconnectAttempts(double multiplier) => _shortTimeout(multiplier) ~/ 5;
|
const Duration _kPauseBetweenReconnectAttempts = Duration(seconds: 1);
|
||||||
|
|
||||||
// See https://github.com/dart-lang/sdk/blob/master/runtime/vm/timeline.cc#L32
|
// See https://github.com/dart-lang/sdk/blob/master/runtime/vm/timeline.cc#L32
|
||||||
String _timelineStreamsToString(List<TimelineStream> streams) {
|
String _timelineStreamsToString(List<TimelineStream> streams) {
|
||||||
@ -116,6 +91,27 @@ String _timelineStreamsToString(List<TimelineStream> streams) {
|
|||||||
|
|
||||||
final Logger _log = Logger('FlutterDriver');
|
final Logger _log = Logger('FlutterDriver');
|
||||||
|
|
||||||
|
Future<T> _warnIfSlow<T>({
|
||||||
|
@required Future<T> future,
|
||||||
|
@required Duration timeout,
|
||||||
|
@required String message,
|
||||||
|
}) {
|
||||||
|
assert(future != null);
|
||||||
|
assert(timeout != null);
|
||||||
|
assert(message != null);
|
||||||
|
return future..timeout(timeout, onTimeout: () { _log.warning(message); });
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration _maxDuration(Duration a, Duration b) {
|
||||||
|
if (a == null)
|
||||||
|
return b;
|
||||||
|
if (b == null)
|
||||||
|
return a;
|
||||||
|
if (a > b)
|
||||||
|
return a;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
/// A convenient accessor to frequently used finders.
|
/// A convenient accessor to frequently used finders.
|
||||||
///
|
///
|
||||||
/// Examples:
|
/// Examples:
|
||||||
@ -142,7 +138,6 @@ class FlutterDriver {
|
|||||||
this._appIsolate, {
|
this._appIsolate, {
|
||||||
bool printCommunication = false,
|
bool printCommunication = false,
|
||||||
bool logCommunicationToFile = true,
|
bool logCommunicationToFile = true,
|
||||||
this.timeoutMultiplier = _kDefaultTimeoutMultiplier,
|
|
||||||
}) : _printCommunication = printCommunication,
|
}) : _printCommunication = printCommunication,
|
||||||
_logCommunicationToFile = logCommunicationToFile,
|
_logCommunicationToFile = logCommunicationToFile,
|
||||||
_driverId = _nextDriverId++;
|
_driverId = _nextDriverId++;
|
||||||
@ -159,41 +154,38 @@ class FlutterDriver {
|
|||||||
///
|
///
|
||||||
/// Resumes the application if it is currently paused (e.g. at a breakpoint).
|
/// Resumes the application if it is currently paused (e.g. at a breakpoint).
|
||||||
///
|
///
|
||||||
/// [dartVmServiceUrl] is the URL to Dart observatory (a.k.a. VM service). If
|
/// `dartVmServiceUrl` is the URL to Dart observatory (a.k.a. VM service). If
|
||||||
/// not specified, the URL specified by the `VM_SERVICE_URL` environment
|
/// not specified, the URL specified by the `VM_SERVICE_URL` environment
|
||||||
/// variable is used. One or the other must be specified.
|
/// variable is used. One or the other must be specified.
|
||||||
///
|
///
|
||||||
/// [printCommunication] determines whether the command communication between
|
/// `printCommunication` determines whether the command communication between
|
||||||
/// the test and the app should be printed to stdout.
|
/// the test and the app should be printed to stdout.
|
||||||
///
|
///
|
||||||
/// [logCommunicationToFile] determines whether the command communication
|
/// `logCommunicationToFile` determines whether the command communication
|
||||||
/// between the test and the app should be logged to `flutter_driver_commands.log`.
|
/// between the test and the app should be logged to `flutter_driver_commands.log`.
|
||||||
///
|
///
|
||||||
/// [FlutterDriver] multiplies all command timeouts by [timeoutMultiplier].
|
/// `isolateNumber` determines the specific isolate to connect to.
|
||||||
///
|
|
||||||
/// [isolateNumber] (optional) determines the specific isolate to connect to.
|
|
||||||
/// If this is left as `null`, will connect to the first isolate found
|
/// If this is left as `null`, will connect to the first isolate found
|
||||||
/// running on [dartVmServiceUrl].
|
/// running on `dartVmServiceUrl`.
|
||||||
///
|
///
|
||||||
/// [isolateReadyTimeout] determines how long after we connect to the VM
|
/// `fuchsiaModuleTarget` specifies the pattern for determining which mod to
|
||||||
/// service we will wait for the first isolate to become runnable. Explicitly
|
/// control. When running on a Fuchsia device, either this or the environment
|
||||||
/// specified non-null values are not affected by [timeoutMultiplier].
|
/// variable `FUCHSIA_MODULE_TARGET` must be set (the environment variable is
|
||||||
|
/// treated as a substring pattern). This field will be ignored if
|
||||||
|
/// `isolateNumber` is set, as this is already enough information to connect
|
||||||
|
/// to an isolate.
|
||||||
///
|
///
|
||||||
/// [fuchsiaModuleTarget] (optional) If running on a Fuchsia Device, either
|
/// The return value is a future. This method never times out, though it may
|
||||||
/// this or the environment variable `FUCHSIA_MODULE_TARGET` must be set. This
|
/// fail (completing with an error). A timeout can be applied by the caller
|
||||||
/// field will be ignored if [isolateNumber] is set, as this is already
|
/// using [Future.timeout] if necessary.
|
||||||
/// enough information to connect to an Isolate.
|
|
||||||
static Future<FlutterDriver> connect({
|
static Future<FlutterDriver> connect({
|
||||||
String dartVmServiceUrl,
|
String dartVmServiceUrl,
|
||||||
bool printCommunication = false,
|
bool printCommunication = false,
|
||||||
bool logCommunicationToFile = true,
|
bool logCommunicationToFile = true,
|
||||||
double timeoutMultiplier = _kDefaultTimeoutMultiplier,
|
|
||||||
int isolateNumber,
|
int isolateNumber,
|
||||||
Duration isolateReadyTimeout,
|
|
||||||
Pattern fuchsiaModuleTarget,
|
Pattern fuchsiaModuleTarget,
|
||||||
}) async {
|
}) async {
|
||||||
isolateReadyTimeout ??= _isolateLoadRunnableTimeout(timeoutMultiplier);
|
// If running on a Fuchsia device, connect to the first isolate whose name
|
||||||
// If running on a Fuchsia device, connect to the first Isolate whose name
|
|
||||||
// matches FUCHSIA_MODULE_TARGET.
|
// matches FUCHSIA_MODULE_TARGET.
|
||||||
//
|
//
|
||||||
// If the user has already supplied an isolate number/URL to the Dart VM
|
// If the user has already supplied an isolate number/URL to the Dart VM
|
||||||
@ -204,16 +196,17 @@ class FlutterDriver {
|
|||||||
flutterDriverLog.listen(print);
|
flutterDriverLog.listen(print);
|
||||||
fuchsiaModuleTarget ??= Platform.environment['FUCHSIA_MODULE_TARGET'];
|
fuchsiaModuleTarget ??= Platform.environment['FUCHSIA_MODULE_TARGET'];
|
||||||
if (fuchsiaModuleTarget == null) {
|
if (fuchsiaModuleTarget == null) {
|
||||||
throw DriverError('No Fuchsia module target has been specified.\n'
|
throw DriverError(
|
||||||
'Please make sure to specify the FUCHSIA_MODULE_TARGET\n'
|
'No Fuchsia module target has been specified.\n'
|
||||||
'environment variable.');
|
'Please make sure to specify the FUCHSIA_MODULE_TARGET '
|
||||||
|
'environment variable.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final fuchsia.FuchsiaRemoteConnection fuchsiaConnection =
|
final fuchsia.FuchsiaRemoteConnection fuchsiaConnection =
|
||||||
await FuchsiaCompat.connect();
|
await FuchsiaCompat.connect();
|
||||||
final List<fuchsia.IsolateRef> refs =
|
final List<fuchsia.IsolateRef> refs =
|
||||||
await fuchsiaConnection.getMainIsolatesByPattern(fuchsiaModuleTarget);
|
await fuchsiaConnection.getMainIsolatesByPattern(fuchsiaModuleTarget);
|
||||||
final fuchsia.IsolateRef ref = refs.first;
|
final fuchsia.IsolateRef ref = refs.first;
|
||||||
await Future<void>.delayed(_fuchsiaDriveDelay(timeoutMultiplier));
|
|
||||||
isolateNumber = ref.number;
|
isolateNumber = ref.number;
|
||||||
dartVmServiceUrl = ref.dartVm.uri.toString();
|
dartVmServiceUrl = ref.dartVm.uri.toString();
|
||||||
await fuchsiaConnection.stop();
|
await fuchsiaConnection.stop();
|
||||||
@ -225,13 +218,13 @@ class FlutterDriver {
|
|||||||
if (dartVmServiceUrl == null) {
|
if (dartVmServiceUrl == null) {
|
||||||
throw DriverError(
|
throw DriverError(
|
||||||
'Could not determine URL to connect to application.\n'
|
'Could not determine URL to connect to application.\n'
|
||||||
'Either the VM_SERVICE_URL environment variable should be set, or an explicit\n'
|
'Either the VM_SERVICE_URL environment variable should be set, or an explicit '
|
||||||
'URL should be provided to the FlutterDriver.connect() method.');
|
'URL should be provided to the FlutterDriver.connect() method.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect to Dart VM services
|
// Connect to Dart VM services
|
||||||
_log.info('Connecting to Flutter application at $dartVmServiceUrl');
|
_log.info('Connecting to Flutter application at $dartVmServiceUrl');
|
||||||
connectionTimeoutMultiplier = timeoutMultiplier;
|
|
||||||
final VMServiceClientConnection connection =
|
final VMServiceClientConnection connection =
|
||||||
await vmServiceConnectFunction(dartVmServiceUrl);
|
await vmServiceConnectFunction(dartVmServiceUrl);
|
||||||
final VMServiceClient client = connection.client;
|
final VMServiceClient client = connection.client;
|
||||||
@ -242,12 +235,7 @@ class FlutterDriver {
|
|||||||
(VMIsolateRef isolate) => isolate.number == isolateNumber);
|
(VMIsolateRef isolate) => isolate.number == isolateNumber);
|
||||||
_log.trace('Isolate found with number: ${isolateRef.number}');
|
_log.trace('Isolate found with number: ${isolateRef.number}');
|
||||||
|
|
||||||
VMIsolate isolate = await isolateRef
|
VMIsolate isolate = await isolateRef.loadRunnable();
|
||||||
.loadRunnable()
|
|
||||||
.timeout(isolateReadyTimeout, onTimeout: () {
|
|
||||||
throw TimeoutException(
|
|
||||||
'Timeout while waiting for the isolate to become runnable');
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO(yjbanov): vm_service_client does not support "None" pause event yet.
|
// TODO(yjbanov): vm_service_client does not support "None" pause event yet.
|
||||||
// It is currently reported as null, but we cannot rely on it because
|
// It is currently reported as null, but we cannot rely on it because
|
||||||
@ -262,7 +250,6 @@ class FlutterDriver {
|
|||||||
isolate.pauseEvent is! VMPauseExceptionEvent &&
|
isolate.pauseEvent is! VMPauseExceptionEvent &&
|
||||||
isolate.pauseEvent is! VMPauseInterruptedEvent &&
|
isolate.pauseEvent is! VMPauseInterruptedEvent &&
|
||||||
isolate.pauseEvent is! VMResumeEvent) {
|
isolate.pauseEvent is! VMResumeEvent) {
|
||||||
await Future<void>.delayed(_shortTimeout(timeoutMultiplier) ~/ 10);
|
|
||||||
isolate = await isolateRef.loadRunnable();
|
isolate = await isolateRef.loadRunnable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,7 +257,6 @@ class FlutterDriver {
|
|||||||
client, connection.peer, isolate,
|
client, connection.peer, isolate,
|
||||||
printCommunication: printCommunication,
|
printCommunication: printCommunication,
|
||||||
logCommunicationToFile: logCommunicationToFile,
|
logCommunicationToFile: logCommunicationToFile,
|
||||||
timeoutMultiplier: timeoutMultiplier,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Attempts to resume the isolate, but does not crash if it fails because
|
// Attempts to resume the isolate, but does not crash if it fails because
|
||||||
@ -323,23 +309,21 @@ class FlutterDriver {
|
|||||||
// option, then the VM service extension is not registered yet. Wait for
|
// option, then the VM service extension is not registered yet. Wait for
|
||||||
// it to be registered.
|
// it to be registered.
|
||||||
await enableIsolateStreams();
|
await enableIsolateStreams();
|
||||||
final Future<dynamic> whenServiceExtensionReady = waitForServiceExtension();
|
final Future<String> whenServiceExtensionReady = waitForServiceExtension();
|
||||||
final Future<dynamic> whenResumed = resumeLeniently();
|
final Future<dynamic> whenResumed = resumeLeniently();
|
||||||
await whenResumed;
|
await whenResumed;
|
||||||
|
|
||||||
try {
|
|
||||||
_log.trace('Waiting for service extension');
|
_log.trace('Waiting for service extension');
|
||||||
// We will never receive the extension event if the user does not
|
// We will never receive the extension event if the user does not
|
||||||
// register it. If that happens time out.
|
// register it. If that happens, show a message but continue waiting.
|
||||||
await whenServiceExtensionReady.timeout(_longTimeout(timeoutMultiplier) * 2);
|
await _warnIfSlow<String>(
|
||||||
} on TimeoutException catch (_) {
|
future: whenServiceExtensionReady,
|
||||||
throw DriverError(
|
timeout: _kUnusuallyLongTimeout,
|
||||||
'Timed out waiting for Flutter Driver extension to become available. '
|
message: 'Flutter Driver extension is taking a long time to become available. '
|
||||||
'Ensure your test app (often: lib/main.dart) imports '
|
'Ensure your test app (often "lib/main.dart") imports '
|
||||||
'"package:flutter_driver/driver_extension.dart" and '
|
'"package:flutter_driver/driver_extension.dart" and '
|
||||||
'calls enableFlutterDriverExtension() as the first call in main().'
|
'calls enableFlutterDriverExtension() as the first call in main().'
|
||||||
);
|
);
|
||||||
}
|
|
||||||
} else if (isolate.pauseEvent is VMPauseExitEvent ||
|
} else if (isolate.pauseEvent is VMPauseExitEvent ||
|
||||||
isolate.pauseEvent is VMPauseBreakpointEvent ||
|
isolate.pauseEvent is VMPauseBreakpointEvent ||
|
||||||
isolate.pauseEvent is VMPauseExceptionEvent ||
|
isolate.pauseEvent is VMPauseExceptionEvent ||
|
||||||
@ -372,7 +356,7 @@ class FlutterDriver {
|
|||||||
'registered.'
|
'registered.'
|
||||||
);
|
);
|
||||||
await enableIsolateStreams();
|
await enableIsolateStreams();
|
||||||
await waitForServiceExtension().timeout(_longTimeout(timeoutMultiplier) * 2);
|
await waitForServiceExtension();
|
||||||
return driver.checkHealth();
|
return driver.checkHealth();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -405,29 +389,21 @@ class FlutterDriver {
|
|||||||
/// Whether to log communication between host and app to `flutter_driver_commands.log`.
|
/// Whether to log communication between host and app to `flutter_driver_commands.log`.
|
||||||
final bool _logCommunicationToFile;
|
final bool _logCommunicationToFile;
|
||||||
|
|
||||||
/// [FlutterDriver] multiplies all command timeouts by this number.
|
|
||||||
///
|
|
||||||
/// The right amount of time a driver command should be given to complete
|
|
||||||
/// depends on various environmental factors, such as the speed of the
|
|
||||||
/// device or the emulator, connection speed and latency, and others. Use
|
|
||||||
/// this multiplier to tailor the timeouts to your environment.
|
|
||||||
final double timeoutMultiplier;
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>> _sendCommand(Command command) async {
|
Future<Map<String, dynamic>> _sendCommand(Command command) async {
|
||||||
Map<String, dynamic> response;
|
Map<String, dynamic> response;
|
||||||
try {
|
try {
|
||||||
final Map<String, String> serialized = command.serialize();
|
final Map<String, String> serialized = command.serialize();
|
||||||
_logCommunication('>>> $serialized');
|
_logCommunication('>>> $serialized');
|
||||||
response = await _appIsolate
|
final Future<Map<String, dynamic>> future = _appIsolate.invokeExtension(
|
||||||
.invokeExtension(_flutterExtensionMethodName, serialized)
|
_flutterExtensionMethodName,
|
||||||
.timeout(command.timeout + _rpcGraceTime(timeoutMultiplier));
|
serialized,
|
||||||
_logCommunication('<<< $response');
|
).then<Map<String, dynamic>>((Object value) => value);
|
||||||
} on TimeoutException catch (error, stackTrace) {
|
response = await _warnIfSlow<Map<String, dynamic>>(
|
||||||
throw DriverError(
|
future: future,
|
||||||
'Failed to fulfill ${command.runtimeType}: Flutter application not responding',
|
timeout: _maxDuration(command.timeout, _kUnusuallyLongTimeout),
|
||||||
error,
|
message: '${command.kind} message is taking a long time to complete...',
|
||||||
stackTrace,
|
|
||||||
);
|
);
|
||||||
|
_logCommunication('<<< $response');
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
throw DriverError(
|
throw DriverError(
|
||||||
'Failed to fulfill ${command.runtimeType} due to remote error',
|
'Failed to fulfill ${command.runtimeType} due to remote error',
|
||||||
@ -452,31 +428,26 @@ class FlutterDriver {
|
|||||||
|
|
||||||
/// Checks the status of the Flutter Driver extension.
|
/// Checks the status of the Flutter Driver extension.
|
||||||
Future<Health> checkHealth({Duration timeout}) async {
|
Future<Health> checkHealth({Duration timeout}) async {
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
|
||||||
return Health.fromJson(await _sendCommand(GetHealth(timeout: timeout)));
|
return Health.fromJson(await _sendCommand(GetHealth(timeout: timeout)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a dump of the render tree.
|
/// Returns a dump of the render tree.
|
||||||
Future<RenderTree> getRenderTree({Duration timeout}) async {
|
Future<RenderTree> getRenderTree({Duration timeout}) async {
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
|
||||||
return RenderTree.fromJson(await _sendCommand(GetRenderTree(timeout: timeout)));
|
return RenderTree.fromJson(await _sendCommand(GetRenderTree(timeout: timeout)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Taps at the center of the widget located by [finder].
|
/// Taps at the center of the widget located by [finder].
|
||||||
Future<void> tap(SerializableFinder finder, {Duration timeout}) async {
|
Future<void> tap(SerializableFinder finder, {Duration timeout}) async {
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
|
||||||
await _sendCommand(Tap(finder, timeout: timeout));
|
await _sendCommand(Tap(finder, timeout: timeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Waits until [finder] locates the target.
|
/// Waits until [finder] locates the target.
|
||||||
Future<void> waitFor(SerializableFinder finder, {Duration timeout}) async {
|
Future<void> waitFor(SerializableFinder finder, {Duration timeout}) async {
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
|
||||||
await _sendCommand(WaitFor(finder, timeout: timeout));
|
await _sendCommand(WaitFor(finder, timeout: timeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Waits until [finder] can no longer locate the target.
|
/// Waits until [finder] can no longer locate the target.
|
||||||
Future<void> waitForAbsent(SerializableFinder finder, {Duration timeout}) async {
|
Future<void> waitForAbsent(SerializableFinder finder, {Duration timeout}) async {
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
|
||||||
await _sendCommand(WaitForAbsent(finder, timeout: timeout));
|
await _sendCommand(WaitForAbsent(finder, timeout: timeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -485,7 +456,6 @@ class FlutterDriver {
|
|||||||
/// Use this method when you need to wait for the moment when the application
|
/// Use this method when you need to wait for the moment when the application
|
||||||
/// becomes "stable", for example, prior to taking a [screenshot].
|
/// becomes "stable", for example, prior to taking a [screenshot].
|
||||||
Future<void> waitUntilNoTransientCallbacks({Duration timeout}) async {
|
Future<void> waitUntilNoTransientCallbacks({Duration timeout}) async {
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
|
||||||
await _sendCommand(WaitUntilNoTransientCallbacks(timeout: timeout));
|
await _sendCommand(WaitUntilNoTransientCallbacks(timeout: timeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -503,7 +473,6 @@ class FlutterDriver {
|
|||||||
/// The move events are generated at a given [frequency] in Hz (or events per
|
/// The move events are generated at a given [frequency] in Hz (or events per
|
||||||
/// second). It defaults to 60Hz.
|
/// second). It defaults to 60Hz.
|
||||||
Future<void> scroll(SerializableFinder finder, double dx, double dy, Duration duration, { int frequency = 60, Duration timeout }) async {
|
Future<void> scroll(SerializableFinder finder, double dx, double dy, Duration duration, { int frequency = 60, Duration timeout }) async {
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
|
||||||
await _sendCommand(Scroll(finder, dx, dy, duration, frequency, timeout: timeout));
|
await _sendCommand(Scroll(finder, dx, dy, duration, frequency, timeout: timeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -515,7 +484,6 @@ class FlutterDriver {
|
|||||||
/// then this method may fail because [finder] doesn't actually exist.
|
/// then this method may fail because [finder] doesn't actually exist.
|
||||||
/// The [scrollUntilVisible] method can be used in this case.
|
/// The [scrollUntilVisible] method can be used in this case.
|
||||||
Future<void> scrollIntoView(SerializableFinder finder, { double alignment = 0.0, Duration timeout }) async {
|
Future<void> scrollIntoView(SerializableFinder finder, { double alignment = 0.0, Duration timeout }) async {
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
|
||||||
await _sendCommand(ScrollIntoView(finder, alignment: alignment, timeout: timeout));
|
await _sendCommand(ScrollIntoView(finder, alignment: alignment, timeout: timeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -540,12 +508,12 @@ class FlutterDriver {
|
|||||||
/// for [dyScroll].
|
/// for [dyScroll].
|
||||||
///
|
///
|
||||||
/// The [timeout] value should be long enough to accommodate as many scrolls
|
/// The [timeout] value should be long enough to accommodate as many scrolls
|
||||||
/// as needed to bring an item into view. The default is 10 seconds.
|
/// as needed to bring an item into view. The default is to not time out.
|
||||||
Future<void> scrollUntilVisible(SerializableFinder scrollable, SerializableFinder item, {
|
Future<void> scrollUntilVisible(SerializableFinder scrollable, SerializableFinder item, {
|
||||||
double alignment = 0.0,
|
double alignment = 0.0,
|
||||||
double dxScroll = 0.0,
|
double dxScroll = 0.0,
|
||||||
double dyScroll = 0.0,
|
double dyScroll = 0.0,
|
||||||
Duration timeout = const Duration(seconds: 10),
|
Duration timeout,
|
||||||
}) async {
|
}) async {
|
||||||
assert(scrollable != null);
|
assert(scrollable != null);
|
||||||
assert(item != null);
|
assert(item != null);
|
||||||
@ -553,7 +521,6 @@ class FlutterDriver {
|
|||||||
assert(dxScroll != null);
|
assert(dxScroll != null);
|
||||||
assert(dyScroll != null);
|
assert(dyScroll != null);
|
||||||
assert(dxScroll != 0.0 || dyScroll != 0.0);
|
assert(dxScroll != 0.0 || dyScroll != 0.0);
|
||||||
assert(timeout != null);
|
|
||||||
|
|
||||||
// Kick off an (unawaited) waitFor that will complete when the item we're
|
// Kick off an (unawaited) waitFor that will complete when the item we're
|
||||||
// looking for finally scrolls onscreen. We add an initial pause to give it
|
// looking for finally scrolls onscreen. We add an initial pause to give it
|
||||||
@ -572,7 +539,6 @@ class FlutterDriver {
|
|||||||
|
|
||||||
/// Returns the text in the `Text` widget located by [finder].
|
/// Returns the text in the `Text` widget located by [finder].
|
||||||
Future<String> getText(SerializableFinder finder, { Duration timeout }) async {
|
Future<String> getText(SerializableFinder finder, { Duration timeout }) async {
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
|
||||||
return GetTextResult.fromJson(await _sendCommand(GetText(finder, timeout: timeout))).text;
|
return GetTextResult.fromJson(await _sendCommand(GetText(finder, timeout: timeout))).text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -609,7 +575,6 @@ class FlutterDriver {
|
|||||||
/// });
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
Future<void> enterText(String text, { Duration timeout }) async {
|
Future<void> enterText(String text, { Duration timeout }) async {
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
|
||||||
await _sendCommand(EnterText(text, timeout: timeout));
|
await _sendCommand(EnterText(text, timeout: timeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -628,7 +593,6 @@ class FlutterDriver {
|
|||||||
/// channel will be mocked out.
|
/// channel will be mocked out.
|
||||||
Future<void> setTextEntryEmulation({ @required bool enabled, Duration timeout }) async {
|
Future<void> setTextEntryEmulation({ @required bool enabled, Duration timeout }) async {
|
||||||
assert(enabled != null);
|
assert(enabled != null);
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
|
||||||
await _sendCommand(SetTextEntryEmulation(enabled, timeout: timeout));
|
await _sendCommand(SetTextEntryEmulation(enabled, timeout: timeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -639,7 +603,6 @@ class FlutterDriver {
|
|||||||
/// callback in [enableFlutterDriverExtension] that can successfully handle
|
/// callback in [enableFlutterDriverExtension] that can successfully handle
|
||||||
/// these requests.
|
/// these requests.
|
||||||
Future<String> requestData(String message, { Duration timeout }) async {
|
Future<String> requestData(String message, { Duration timeout }) async {
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
|
||||||
return RequestDataResult.fromJson(await _sendCommand(RequestData(message, timeout: timeout))).message;
|
return RequestDataResult.fromJson(await _sendCommand(RequestData(message, timeout: timeout))).message;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -648,7 +611,6 @@ class FlutterDriver {
|
|||||||
/// Returns true when the call actually changed the state from on to off or
|
/// Returns true when the call actually changed the state from on to off or
|
||||||
/// vice versa.
|
/// vice versa.
|
||||||
Future<bool> setSemantics(bool enabled, { Duration timeout }) async {
|
Future<bool> setSemantics(bool enabled, { Duration timeout }) async {
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
|
||||||
final SetSemanticsResult result = SetSemanticsResult.fromJson(await _sendCommand(SetSemantics(enabled, timeout: timeout)));
|
final SetSemanticsResult result = SetSemanticsResult.fromJson(await _sendCommand(SetSemantics(enabled, timeout: timeout)));
|
||||||
return result.changedState;
|
return result.changedState;
|
||||||
}
|
}
|
||||||
@ -662,16 +624,15 @@ class FlutterDriver {
|
|||||||
/// Semantics must be enabled to use this method, either using a platform
|
/// Semantics must be enabled to use this method, either using a platform
|
||||||
/// specific shell command or [setSemantics].
|
/// specific shell command or [setSemantics].
|
||||||
Future<int> getSemanticsId(SerializableFinder finder, { Duration timeout }) async {
|
Future<int> getSemanticsId(SerializableFinder finder, { Duration timeout }) async {
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
|
||||||
final Map<String, dynamic> jsonResponse = await _sendCommand(GetSemanticsId(finder, timeout: timeout));
|
final Map<String, dynamic> jsonResponse = await _sendCommand(GetSemanticsId(finder, timeout: timeout));
|
||||||
final GetSemanticsIdResult result = GetSemanticsIdResult.fromJson(jsonResponse);
|
final GetSemanticsIdResult result = GetSemanticsIdResult.fromJson(jsonResponse);
|
||||||
return result.id;
|
return result.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Take a screenshot. The image will be returned as a PNG.
|
/// Take a screenshot.
|
||||||
Future<List<int>> screenshot({ Duration timeout }) async {
|
///
|
||||||
timeout ??= _longTimeout(timeoutMultiplier);
|
/// The image will be returned as a PNG.
|
||||||
|
Future<List<int>> screenshot() async {
|
||||||
// HACK: this artificial delay here is to deal with a race between the
|
// HACK: this artificial delay here is to deal with a race between the
|
||||||
// driver script and the GPU thread. The issue is that driver API
|
// driver script and the GPU thread. The issue is that driver API
|
||||||
// synchronizes with the framework based on transient callbacks, which
|
// synchronizes with the framework based on transient callbacks, which
|
||||||
@ -679,7 +640,7 @@ class FlutterDriver {
|
|||||||
// in ASCII art:
|
// in ASCII art:
|
||||||
//
|
//
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// Before this change:
|
// Without this delay:
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// UI : <-- build -->
|
// UI : <-- build -->
|
||||||
// GPU : <-- rasterize -->
|
// GPU : <-- rasterize -->
|
||||||
@ -690,12 +651,13 @@ class FlutterDriver {
|
|||||||
// action taken, such as a `tap()`, and the subsequent call to
|
// action taken, such as a `tap()`, and the subsequent call to
|
||||||
// `screenshot()`. The gap is random because it is determined by the
|
// `screenshot()`. The gap is random because it is determined by the
|
||||||
// unpredictable network communication between the driver process and
|
// unpredictable network communication between the driver process and
|
||||||
// the application. If this gap is too short, the screenshot is taken
|
// the application. If this gap is too short, which it typically will
|
||||||
// before the GPU thread is done rasterizing the frame, so the
|
// be, the screenshot is taken before the GPU thread is done
|
||||||
// screenshot of the previous frame is taken, which is wrong.
|
// rasterizing the frame, so the screenshot of the previous frame is
|
||||||
|
// taken, which is wrong.
|
||||||
//
|
//
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// After this change:
|
// With this delay, if we're lucky:
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// UI : <-- build -->
|
// UI : <-- build -->
|
||||||
// GPU : <-- rasterize -->
|
// GPU : <-- rasterize -->
|
||||||
@ -705,16 +667,28 @@ class FlutterDriver {
|
|||||||
// The two-second gap should be long enough for the GPU thread to
|
// The two-second gap should be long enough for the GPU thread to
|
||||||
// finish rasterizing the frame, but not longer than necessary to keep
|
// finish rasterizing the frame, but not longer than necessary to keep
|
||||||
// driver tests as fast a possible.
|
// driver tests as fast a possible.
|
||||||
|
//
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// With this delay, if we're not lucky:
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// UI : <-- build -->
|
||||||
|
// GPU : <-- rasterize randomly slow today -->
|
||||||
|
// Gap : | 2 seconds or more |
|
||||||
|
// Driver: <-- screenshot -->
|
||||||
|
//
|
||||||
|
// In practice, sometimes the device gets really busy for a while and
|
||||||
|
// even two seconds isn't enough, which means that this is still racy
|
||||||
|
// and a source of flakes.
|
||||||
await Future<void>.delayed(const Duration(seconds: 2));
|
await Future<void>.delayed(const Duration(seconds: 2));
|
||||||
|
|
||||||
final Map<String, dynamic> result = await _peer.sendRequest('_flutter.screenshot').timeout(timeout);
|
final Map<String, dynamic> result = await _peer.sendRequest('_flutter.screenshot');
|
||||||
return base64.decode(result['screenshot']);
|
return base64.decode(result['screenshot']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the Flags set in the Dart VM as JSON.
|
/// Returns the Flags set in the Dart VM as JSON.
|
||||||
///
|
///
|
||||||
/// See the complete documentation for `getFlagList` Dart VM service method
|
/// See the complete documentation for [the `getFlagList` Dart VM service
|
||||||
/// [here][getFlagList].
|
/// method][getFlagList].
|
||||||
///
|
///
|
||||||
/// Example return value:
|
/// Example return value:
|
||||||
///
|
///
|
||||||
@ -730,23 +704,30 @@ class FlutterDriver {
|
|||||||
/// ]
|
/// ]
|
||||||
///
|
///
|
||||||
/// [getFlagList]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#getflaglist
|
/// [getFlagList]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#getflaglist
|
||||||
Future<List<Map<String, dynamic>>> getVmFlags({ Duration timeout }) async {
|
Future<List<Map<String, dynamic>>> getVmFlags() async {
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
final Map<String, dynamic> result = await _peer.sendRequest('getFlagList');
|
||||||
final Map<String, dynamic> result = await _peer.sendRequest('getFlagList').timeout(timeout);
|
|
||||||
return result['flags'];
|
return result['flags'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts recording performance traces.
|
/// Starts recording performance traces.
|
||||||
|
///
|
||||||
|
/// The `timeout` argument causes a warning to be displayed to the user if the
|
||||||
|
/// operation exceeds the specified timeout; it does not actually cancel the
|
||||||
|
/// operation.
|
||||||
Future<void> startTracing({
|
Future<void> startTracing({
|
||||||
List<TimelineStream> streams = _defaultStreams,
|
List<TimelineStream> streams = _defaultStreams,
|
||||||
Duration timeout,
|
Duration timeout = _kUnusuallyLongTimeout,
|
||||||
}) async {
|
}) async {
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
|
||||||
assert(streams != null && streams.isNotEmpty);
|
assert(streams != null && streams.isNotEmpty);
|
||||||
|
assert(timeout != null);
|
||||||
try {
|
try {
|
||||||
await _peer.sendRequest(_setVMTimelineFlagsMethodName, <String, String>{
|
await _warnIfSlow<void>(
|
||||||
|
future: _peer.sendRequest(_setVMTimelineFlagsMethodName, <String, String>{
|
||||||
'recordedStreams': _timelineStreamsToString(streams)
|
'recordedStreams': _timelineStreamsToString(streams)
|
||||||
}).timeout(timeout);
|
}),
|
||||||
|
timeout: timeout,
|
||||||
|
message: 'VM is taking an unusually long time to respond to being told to start tracing...',
|
||||||
|
);
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
throw DriverError(
|
throw DriverError(
|
||||||
'Failed to start tracing due to remote error',
|
'Failed to start tracing due to remote error',
|
||||||
@ -757,12 +738,20 @@ class FlutterDriver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Stops recording performance traces and downloads the timeline.
|
/// Stops recording performance traces and downloads the timeline.
|
||||||
Future<Timeline> stopTracingAndDownloadTimeline({ Duration timeout }) async {
|
///
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
/// The `timeout` argument causes a warning to be displayed to the user if the
|
||||||
|
/// operation exceeds the specified timeout; it does not actually cancel the
|
||||||
|
/// operation.
|
||||||
|
Future<Timeline> stopTracingAndDownloadTimeline({
|
||||||
|
Duration timeout = _kUnusuallyLongTimeout,
|
||||||
|
}) async {
|
||||||
|
assert(timeout != null);
|
||||||
try {
|
try {
|
||||||
await _peer
|
await _warnIfSlow<void>(
|
||||||
.sendRequest(_setVMTimelineFlagsMethodName, <String, String>{'recordedStreams': '[]'})
|
future: _peer.sendRequest(_setVMTimelineFlagsMethodName, <String, String>{'recordedStreams': '[]'}),
|
||||||
.timeout(timeout);
|
timeout: timeout,
|
||||||
|
message: 'VM is taking an unusually long time to respond to being told to stop tracing...',
|
||||||
|
);
|
||||||
return Timeline.fromJson(await _peer.sendRequest(_getVMTimelineMethodName));
|
return Timeline.fromJson(await _peer.sendRequest(_getVMTimelineMethodName));
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
throw DriverError(
|
throw DriverError(
|
||||||
@ -801,12 +790,20 @@ class FlutterDriver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Clears all timeline events recorded up until now.
|
/// Clears all timeline events recorded up until now.
|
||||||
Future<void> clearTimeline({ Duration timeout }) async {
|
///
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
/// The `timeout` argument causes a warning to be displayed to the user if the
|
||||||
|
/// operation exceeds the specified timeout; it does not actually cancel the
|
||||||
|
/// operation.
|
||||||
|
Future<void> clearTimeline({
|
||||||
|
Duration timeout = _kUnusuallyLongTimeout
|
||||||
|
}) async {
|
||||||
|
assert(timeout != null);
|
||||||
try {
|
try {
|
||||||
await _peer
|
await _warnIfSlow<void>(
|
||||||
.sendRequest(_clearVMTimelineMethodName, <String, String>{})
|
future: _peer.sendRequest(_clearVMTimelineMethodName, <String, String>{}),
|
||||||
.timeout(timeout);
|
timeout: timeout,
|
||||||
|
message: 'VM is taking an unusually long time to respond to being told to clear its timeline buffer...',
|
||||||
|
);
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
throw DriverError(
|
throw DriverError(
|
||||||
'Failed to clear event timeline due to remote error',
|
'Failed to clear event timeline due to remote error',
|
||||||
@ -833,7 +830,6 @@ class FlutterDriver {
|
|||||||
/// ensure that no action is performed while the app is undergoing a
|
/// ensure that no action is performed while the app is undergoing a
|
||||||
/// transition to avoid flakiness.
|
/// transition to avoid flakiness.
|
||||||
Future<T> runUnsynchronized<T>(Future<T> action(), { Duration timeout }) async {
|
Future<T> runUnsynchronized<T>(Future<T> action(), { Duration timeout }) async {
|
||||||
timeout ??= _shortTimeout(timeoutMultiplier);
|
|
||||||
await _sendCommand(SetFrameSync(false, timeout: timeout));
|
await _sendCommand(SetFrameSync(false, timeout: timeout));
|
||||||
T result;
|
T result;
|
||||||
try {
|
try {
|
||||||
@ -893,11 +889,6 @@ typedef VMServiceConnectFunction = Future<VMServiceClientConnection> Function(St
|
|||||||
///
|
///
|
||||||
/// Overwrite this function if you require a custom method for connecting to
|
/// Overwrite this function if you require a custom method for connecting to
|
||||||
/// the VM service.
|
/// the VM service.
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
///
|
|
||||||
/// * [connectionTimeoutMultiplier], which controls the timeouts while
|
|
||||||
/// establishing a connection using the default connection function.
|
|
||||||
VMServiceConnectFunction vmServiceConnectFunction = _waitAndConnect;
|
VMServiceConnectFunction vmServiceConnectFunction = _waitAndConnect;
|
||||||
|
|
||||||
/// Restores [vmServiceConnectFunction] to its default value.
|
/// Restores [vmServiceConnectFunction] to its default value.
|
||||||
@ -907,44 +898,30 @@ void restoreVmServiceConnectFunction() {
|
|||||||
|
|
||||||
/// Waits for a real Dart VM service to become available, then connects using
|
/// Waits for a real Dart VM service to become available, then connects using
|
||||||
/// the [VMServiceClient].
|
/// the [VMServiceClient].
|
||||||
///
|
|
||||||
/// Times out after 30 seconds.
|
|
||||||
Future<VMServiceClientConnection> _waitAndConnect(String url) async {
|
Future<VMServiceClientConnection> _waitAndConnect(String url) async {
|
||||||
final Stopwatch timer = Stopwatch()..start();
|
|
||||||
|
|
||||||
Future<VMServiceClientConnection> attemptConnection() async {
|
|
||||||
Uri uri = Uri.parse(url);
|
Uri uri = Uri.parse(url);
|
||||||
if (uri.scheme == 'http')
|
if (uri.scheme == 'http')
|
||||||
uri = uri.replace(scheme: 'ws', path: '/ws');
|
uri = uri.replace(scheme: 'ws', path: '/ws');
|
||||||
|
int attempts = 0;
|
||||||
|
while (true) {
|
||||||
WebSocket ws1;
|
WebSocket ws1;
|
||||||
WebSocket ws2;
|
WebSocket ws2;
|
||||||
try {
|
try {
|
||||||
ws1 = await WebSocket.connect(uri.toString()).timeout(_shortTimeout(connectionTimeoutMultiplier));
|
ws1 = await WebSocket.connect(uri.toString());
|
||||||
ws2 = await WebSocket.connect(uri.toString()).timeout(_shortTimeout(connectionTimeoutMultiplier));
|
ws2 = await WebSocket.connect(uri.toString());
|
||||||
return VMServiceClientConnection(
|
return VMServiceClientConnection(
|
||||||
VMServiceClient(IOWebSocketChannel(ws1).cast()),
|
VMServiceClient(IOWebSocketChannel(ws1).cast()),
|
||||||
rpc.Peer(IOWebSocketChannel(ws2).cast())..listen()
|
rpc.Peer(IOWebSocketChannel(ws2).cast())..listen(),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await ws1?.close();
|
await ws1?.close();
|
||||||
await ws2?.close();
|
await ws2?.close();
|
||||||
|
if (attempts > 5)
|
||||||
if (timer.elapsed < _longTimeout(connectionTimeoutMultiplier) * 2) {
|
_log.warning('It is taking an unusually long time to connect to the VM...');
|
||||||
_log.info('Waiting for application to start');
|
attempts += 1;
|
||||||
await Future<void>.delayed(_pauseBetweenReconnectAttempts(connectionTimeoutMultiplier));
|
await Future<void>.delayed(_kPauseBetweenReconnectAttempts);
|
||||||
return attemptConnection();
|
|
||||||
} else {
|
|
||||||
_log.critical(
|
|
||||||
'Application has not started in 30 seconds. '
|
|
||||||
'Giving up.'
|
|
||||||
);
|
|
||||||
rethrow;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return attemptConnection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provides convenient accessors to frequently used finders.
|
/// Provides convenient accessors to frequently used finders.
|
||||||
|
@ -177,7 +177,10 @@ class FlutterDriverExtension {
|
|||||||
if (commandHandler == null || commandDeserializer == null)
|
if (commandHandler == null || commandDeserializer == null)
|
||||||
throw 'Extension $_extensionMethod does not support command $commandKind';
|
throw 'Extension $_extensionMethod does not support command $commandKind';
|
||||||
final Command command = commandDeserializer(params);
|
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());
|
return _makeResponse(response?.toJson());
|
||||||
} on TimeoutException catch (error, stackTrace) {
|
} on TimeoutException catch (error, stackTrace) {
|
||||||
final String msg = 'Timeout while executing $commandKind: $error\n$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:json_rpc_2/json_rpc_2.dart' as rpc;
|
||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
import 'package:vm_service_client/vm_service_client.dart';
|
import 'package:vm_service_client/vm_service_client.dart';
|
||||||
|
import 'package:quiver/testing/async.dart';
|
||||||
|
|
||||||
import 'common.dart';
|
import 'common.dart';
|
||||||
|
|
||||||
/// Magical timeout value that's different from the default.
|
/// Magical timeout value that's different from the default.
|
||||||
const Duration _kTestTimeout = Duration(milliseconds: 1234);
|
const Duration _kTestTimeout = Duration(milliseconds: 1234);
|
||||||
const String _kSerializedTestTimeout = '1234';
|
const String _kSerializedTestTimeout = '1234';
|
||||||
const Duration _kDefaultCommandTimeout = Duration(seconds: 5);
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('FlutterDriver.connect', () {
|
group('FlutterDriver.connect', () {
|
||||||
@ -358,17 +358,19 @@ void main() {
|
|||||||
|
|
||||||
group('sendCommand error conditions', () {
|
group('sendCommand error conditions', () {
|
||||||
test('local timeout', () async {
|
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) {
|
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;
|
return Completer<Map<String, dynamic>>().future;
|
||||||
});
|
});
|
||||||
try {
|
FakeAsync().run((FakeAsync time) {
|
||||||
await driver.waitFor(find.byTooltip('foo'), timeout: const Duration(milliseconds: 100));
|
driver.waitFor(find.byTooltip('foo'));
|
||||||
fail('expected an exception');
|
expect(log, <String>[]);
|
||||||
} catch (error) {
|
time.elapse(const Duration(hours: 1));
|
||||||
expect(error is DriverError, isTrue);
|
});
|
||||||
expect(error.message, 'Failed to fulfill WaitFor: Flutter application not responding');
|
expect(log, <String>['[warning] FlutterDriver: waitFor message is taking a long time to complete...']);
|
||||||
}
|
await logSub.cancel();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('remote error', () async {
|
test('remote error', () async {
|
||||||
@ -389,7 +391,6 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('FlutterDriver with custom timeout', () {
|
group('FlutterDriver with custom timeout', () {
|
||||||
const double kTestMultiplier = 3.0;
|
|
||||||
MockVMServiceClient mockClient;
|
MockVMServiceClient mockClient;
|
||||||
MockPeer mockPeer;
|
MockPeer mockPeer;
|
||||||
MockIsolate mockIsolate;
|
MockIsolate mockIsolate;
|
||||||
@ -399,21 +400,20 @@ void main() {
|
|||||||
mockClient = MockVMServiceClient();
|
mockClient = MockVMServiceClient();
|
||||||
mockPeer = MockPeer();
|
mockPeer = MockPeer();
|
||||||
mockIsolate = MockIsolate();
|
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) {
|
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
|
||||||
expect(i.positionalArguments[1], <String, String>{
|
expect(i.positionalArguments[1], <String, String>{
|
||||||
'command': 'get_health',
|
'command': 'get_health',
|
||||||
'timeout': '${(_kDefaultCommandTimeout * kTestMultiplier).inMilliseconds}',
|
|
||||||
});
|
});
|
||||||
return makeMockResponse(<String, dynamic>{'status': 'ok'});
|
return makeMockResponse(<String, dynamic>{'status': 'ok'});
|
||||||
});
|
});
|
||||||
await driver.checkHealth();
|
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) {
|
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
|
||||||
expect(i.positionalArguments[1], <String, String>{
|
expect(i.positionalArguments[1], <String, String>{
|
||||||
'command': 'get_health',
|
'command': 'get_health',
|
||||||
|
@ -88,15 +88,13 @@ class AndroidDevice extends Device {
|
|||||||
propCommand,
|
propCommand,
|
||||||
stdoutEncoding: latin1,
|
stdoutEncoding: latin1,
|
||||||
stderrEncoding: latin1,
|
stderrEncoding: latin1,
|
||||||
).timeout(const Duration(seconds: 5));
|
);
|
||||||
if (result.exitCode == 0) {
|
if (result.exitCode == 0) {
|
||||||
_properties = parseAdbDeviceProperties(result.stdout);
|
_properties = parseAdbDeviceProperties(result.stdout);
|
||||||
} else {
|
} else {
|
||||||
printError('Error retrieving device properties for $name:');
|
printError('Error retrieving device properties for $name:');
|
||||||
printError(result.stderr);
|
printError(result.stderr);
|
||||||
}
|
}
|
||||||
} on TimeoutException catch (_) {
|
|
||||||
throwToolExit('adb not responding');
|
|
||||||
} on ProcessException catch (error) {
|
} on ProcessException catch (error) {
|
||||||
printError('Error retrieving device properties for $name: $error');
|
printError('Error retrieving device properties for $name: $error');
|
||||||
}
|
}
|
||||||
@ -279,7 +277,7 @@ class AndroidDevice extends Device {
|
|||||||
if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
|
if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion())
|
||||||
return false;
|
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]));
|
final RunResult installResult = await runAsync(adbCommandForDevice(<String>['install', '-t', '-r', apk.file.path]));
|
||||||
status.stop();
|
status.stop();
|
||||||
// Some versions of adb exit with exit code 0 even on failure :(
|
// 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();
|
throw '${runResult.stdout}\n${runResult.stderr}'.trimRight();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// emulator continues running on a successful launch so if we
|
// The emulator continues running on a successful launch, so if it hasn't
|
||||||
// haven't quit within 3 seconds we assume that's a success and just
|
// quit within 3 seconds we assume that's a success and just return. This
|
||||||
// return.
|
// 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>>[
|
return Future.any<void>(<Future<void>>[
|
||||||
launchResult,
|
launchResult,
|
||||||
Future<void>.delayed(const Duration(seconds: 3))
|
Future<void>.delayed(const Duration(seconds: 3))
|
||||||
|
@ -50,9 +50,15 @@ class AndroidWorkflow implements Workflow {
|
|||||||
class AndroidValidator extends DoctorValidator {
|
class AndroidValidator extends DoctorValidator {
|
||||||
AndroidValidator(): super('Android toolchain - develop for Android devices',);
|
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
|
/// Returns false if we cannot determine the Java version or if the version
|
||||||
/// is not compatible.
|
/// is not compatible.
|
||||||
Future<bool> _checkJavaVersion(String javaBinary, List<ValidationMessage> messages) async {
|
Future<bool> _checkJavaVersion(String javaBinary, List<ValidationMessage> messages) async {
|
||||||
|
_task = 'Checking Java status';
|
||||||
|
try {
|
||||||
if (!processManager.canRun(javaBinary)) {
|
if (!processManager.canRun(javaBinary)) {
|
||||||
messages.add(ValidationMessage.error(userMessages.androidCantRunJavaBinary(javaBinary)));
|
messages.add(ValidationMessage.error(userMessages.androidCantRunJavaBinary(javaBinary)));
|
||||||
return false;
|
return false;
|
||||||
@ -76,6 +82,9 @@ class AndroidValidator extends DoctorValidator {
|
|||||||
messages.add(ValidationMessage(userMessages.androidJavaVersion(javaVersion)));
|
messages.add(ValidationMessage(userMessages.androidJavaVersion(javaVersion)));
|
||||||
// TODO(johnmccutchan): Validate version.
|
// TODO(johnmccutchan): Validate version.
|
||||||
return true;
|
return true;
|
||||||
|
} finally {
|
||||||
|
_task = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -149,6 +158,9 @@ class AndroidValidator extends DoctorValidator {
|
|||||||
class AndroidLicenseValidator extends DoctorValidator {
|
class AndroidLicenseValidator extends DoctorValidator {
|
||||||
AndroidLicenseValidator(): super('Android license subvalidator',);
|
AndroidLicenseValidator(): super('Android license subvalidator',);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get slowWarning => 'Checking Android licenses is taking an unexpectedly long time...';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ValidationResult> validate() async {
|
Future<ValidationResult> validate() async {
|
||||||
final List<ValidationMessage> messages = <ValidationMessage>[];
|
final List<ValidationMessage> messages = <ValidationMessage>[];
|
||||||
@ -208,10 +220,8 @@ class AndroidLicenseValidator extends DoctorValidator {
|
|||||||
Future<LicensesAccepted> get licensesAccepted async {
|
Future<LicensesAccepted> get licensesAccepted async {
|
||||||
LicensesAccepted status;
|
LicensesAccepted status;
|
||||||
|
|
||||||
void _onLine(String line) {
|
void _handleLine(String line) {
|
||||||
if (status == null && licenseAccepted.hasMatch(line)) {
|
if (licenseCounts.hasMatch(line)) {
|
||||||
status = LicensesAccepted.all;
|
|
||||||
} else if (licenseCounts.hasMatch(line)) {
|
|
||||||
final Match match = licenseCounts.firstMatch(line);
|
final Match match = licenseCounts.firstMatch(line);
|
||||||
if (match.group(1) != match.group(2)) {
|
if (match.group(1) != match.group(2)) {
|
||||||
status = LicensesAccepted.some;
|
status = LicensesAccepted.some;
|
||||||
@ -219,9 +229,12 @@ class AndroidLicenseValidator extends DoctorValidator {
|
|||||||
status = LicensesAccepted.none;
|
status = LicensesAccepted.none;
|
||||||
}
|
}
|
||||||
} else if (licenseNotAccepted.hasMatch(line)) {
|
} else if (licenseNotAccepted.hasMatch(line)) {
|
||||||
// In case the format changes, a more general match will keep doctor
|
// The licenseNotAccepted pattern is trying to match the same line as
|
||||||
// mostly working.
|
// licenseCounts, but is more general. In case the format changes, a
|
||||||
|
// more general match may keep doctor mostly working.
|
||||||
status = LicensesAccepted.none;
|
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
|
final Future<void> output = process.stdout
|
||||||
.transform<String>(const Utf8Decoder(allowMalformed: true))
|
.transform<String>(const Utf8Decoder(allowMalformed: true))
|
||||||
.transform<String>(const LineSplitter())
|
.transform<String>(const LineSplitter())
|
||||||
.listen(_onLine)
|
.listen(_handleLine)
|
||||||
.asFuture<void>(null);
|
.asFuture<void>(null);
|
||||||
final Future<void> errors = process.stderr
|
final Future<void> errors = process.stderr
|
||||||
.transform<String>(const Utf8Decoder(allowMalformed: true))
|
.transform<String>(const Utf8Decoder(allowMalformed: true))
|
||||||
.transform<String>(const LineSplitter())
|
.transform<String>(const LineSplitter())
|
||||||
.listen(_onLine)
|
.listen(_handleLine)
|
||||||
.asFuture<void>(null);
|
.asFuture<void>(null);
|
||||||
try {
|
await Future.wait<void>(<Future<void>>[output, errors]);
|
||||||
await Future.wait<void>(<Future<void>>[output, errors]).timeout(const Duration(seconds: 30));
|
|
||||||
} catch (TimeoutException) {
|
|
||||||
printTrace(userMessages.androidLicensesTimeout(androidSdk.sdkManagerPath));
|
|
||||||
processManager.killPid(process.pid);
|
|
||||||
}
|
|
||||||
return status ?? LicensesAccepted.unknown;
|
return status ?? LicensesAccepted.unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,9 +269,10 @@ class AndroidLicenseValidator extends DoctorValidator {
|
|||||||
_ensureCanRunSdkManager();
|
_ensureCanRunSdkManager();
|
||||||
|
|
||||||
final Version sdkManagerVersion = Version.parse(androidSdk.sdkManagerVersion);
|
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.
|
// SDK manager is found, but needs to be updated.
|
||||||
throwToolExit(userMessages.androidSdkOutdated(androidSdk.sdkManagerPath));
|
throwToolExit(userMessages.androidSdkOutdated(androidSdk.sdkManagerPath));
|
||||||
|
}
|
||||||
|
|
||||||
final Process process = await runCommand(
|
final Process process = await runCommand(
|
||||||
<String>[androidSdk.sdkManagerPath, '--licenses'],
|
<String>[androidSdk.sdkManagerPath, '--licenses'],
|
||||||
|
@ -97,7 +97,7 @@ Future<GradleProject> _readGradleProject() async {
|
|||||||
final FlutterProject flutterProject = await FlutterProject.current();
|
final FlutterProject flutterProject = await FlutterProject.current();
|
||||||
final String gradle = await _ensureGradle(flutterProject);
|
final String gradle = await _ensureGradle(flutterProject);
|
||||||
updateLocalProperties(project: flutterProject);
|
updateLocalProperties(project: flutterProject);
|
||||||
final Status status = logger.startProgress('Resolving dependencies...', expectSlowOperation: true);
|
final Status status = logger.startProgress('Resolving dependencies...', timeout: kSlowOperation);
|
||||||
GradleProject project;
|
GradleProject project;
|
||||||
try {
|
try {
|
||||||
final RunResult propertiesRunResult = await runCheckedAsync(
|
final RunResult propertiesRunResult = await runCheckedAsync(
|
||||||
@ -175,7 +175,7 @@ Future<String> _ensureGradle(FlutterProject project) async {
|
|||||||
// of validating the Gradle executable. This may take several seconds.
|
// of validating the Gradle executable. This may take several seconds.
|
||||||
Future<String> _initializeGradle(FlutterProject project) async {
|
Future<String> _initializeGradle(FlutterProject project) async {
|
||||||
final Directory android = project.android.hostAppGradleRoot;
|
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);
|
String gradle = _locateGradlewExecutable(android);
|
||||||
if (gradle == null) {
|
if (gradle == null) {
|
||||||
injectGradleWrapper(android);
|
injectGradleWrapper(android);
|
||||||
@ -314,8 +314,8 @@ Future<void> buildGradleProject({
|
|||||||
Future<void> _buildGradleProjectV1(FlutterProject project, String gradle) async {
|
Future<void> _buildGradleProjectV1(FlutterProject project, String gradle) async {
|
||||||
// Run 'gradlew build'.
|
// Run 'gradlew build'.
|
||||||
final Status status = logger.startProgress(
|
final Status status = logger.startProgress(
|
||||||
"Running 'gradlew build'...",
|
'Running \'gradlew build\'...',
|
||||||
expectSlowOperation: true,
|
timeout: kSlowOperation,
|
||||||
multilineOutput: true,
|
multilineOutput: true,
|
||||||
);
|
);
|
||||||
final int exitCode = await runCommandAndStreamOutput(
|
final int exitCode = await runCommandAndStreamOutput(
|
||||||
@ -365,8 +365,8 @@ Future<void> _buildGradleProjectV2(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
final Status status = logger.startProgress(
|
final Status status = logger.startProgress(
|
||||||
"Gradle task '$assembleTask'...",
|
'Running Gradle task \'$assembleTask\'...',
|
||||||
expectSlowOperation: true,
|
timeout: kSlowOperation,
|
||||||
multilineOutput: true,
|
multilineOutput: true,
|
||||||
);
|
);
|
||||||
final String gradlePath = fs.file(gradle).absolute.path;
|
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
|
/// Gets the value associated with the specified [type], or `null` if no
|
||||||
/// such value has been associated.
|
/// such value has been associated.
|
||||||
dynamic operator [](Type type) {
|
Object operator [](Type type) {
|
||||||
dynamic value = _generateIfNecessary(type, _overrides);
|
dynamic value = _generateIfNecessary(type, _overrides);
|
||||||
if (value == null && _parent != null)
|
if (value == null && _parent != null)
|
||||||
value = _parent[type];
|
value = _parent[type];
|
||||||
|
@ -36,9 +36,7 @@ RecordingFileSystem getRecordingFileSystem(String location) {
|
|||||||
final RecordingFileSystem fileSystem = RecordingFileSystem(
|
final RecordingFileSystem fileSystem = RecordingFileSystem(
|
||||||
delegate: _kLocalFs, destination: dir);
|
delegate: _kLocalFs, destination: dir);
|
||||||
addShutdownHook(() async {
|
addShutdownHook(() async {
|
||||||
await fileSystem.recording.flush(
|
await fileSystem.recording.flush();
|
||||||
pendingResultTimeout: const Duration(seconds: 5),
|
|
||||||
);
|
|
||||||
}, ShutdownStage.SERIALIZE_RECORDING);
|
}, ShutdownStage.SERIALIZE_RECORDING);
|
||||||
return fileSystem;
|
return fileSystem;
|
||||||
}
|
}
|
||||||
|
@ -162,10 +162,7 @@ class Stdio {
|
|||||||
bool get supportsAnsiEscapes => hasTerminal ? io.stdout.supportsAnsiEscapes : false;
|
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];
|
Stdio get stdio => context[Stdio];
|
||||||
|
io.IOSink get stdout => stdio.stdout;
|
||||||
|
Stream<List<int>> get stdin => stdio.stdin;
|
||||||
|
io.IOSink get stderr => stdio.stderr;
|
||||||
|
@ -12,6 +12,8 @@ import 'terminal.dart';
|
|||||||
import 'utils.dart';
|
import 'utils.dart';
|
||||||
|
|
||||||
const int kDefaultStatusPadding = 59;
|
const int kDefaultStatusPadding = 59;
|
||||||
|
const Duration kFastOperation = Duration(seconds: 2);
|
||||||
|
const Duration kSlowOperation = Duration(minutes: 2);
|
||||||
|
|
||||||
typedef VoidCallback = void Function();
|
typedef VoidCallback = void Function();
|
||||||
|
|
||||||
@ -24,24 +26,30 @@ abstract class Logger {
|
|||||||
|
|
||||||
bool get hasTerminal => stdio.hasTerminal;
|
bool get hasTerminal => stdio.hasTerminal;
|
||||||
|
|
||||||
/// Display an error [message] to the user. Commands should use this if they
|
/// Display an error `message` to the user. Commands should use this if they
|
||||||
/// fail in some way.
|
/// fail in some way.
|
||||||
///
|
///
|
||||||
/// The [message] argument is printed to the stderr in red by default.
|
/// The `message` argument is printed to the stderr in red by default.
|
||||||
/// The [stackTrace] argument is the stack trace that will be printed if
|
///
|
||||||
|
/// The `stackTrace` argument is the stack trace that will be printed if
|
||||||
/// supplied.
|
/// supplied.
|
||||||
/// The [emphasis] argument will cause the output message be printed in bold text.
|
///
|
||||||
/// The [color] argument will print the message in the supplied color instead
|
/// The `emphasis` argument will cause the output message be printed in bold text.
|
||||||
|
///
|
||||||
|
/// The `color` argument will print the message in the supplied color instead
|
||||||
/// of the default of red. Colors will not be printed if the output terminal
|
/// of the default of red. Colors will not be printed if the output terminal
|
||||||
/// doesn't support them.
|
/// doesn't support them.
|
||||||
/// The [indent] argument specifies the number of spaces to indent the overall
|
///
|
||||||
|
/// The `indent` argument specifies the number of spaces to indent the overall
|
||||||
/// message. If wrapping is enabled in [outputPreferences], then the wrapped
|
/// message. If wrapping is enabled in [outputPreferences], then the wrapped
|
||||||
/// lines will be indented as well.
|
/// lines will be indented as well.
|
||||||
/// If [hangingIndent] is specified, then any wrapped lines will be indented
|
///
|
||||||
|
/// If `hangingIndent` is specified, then any wrapped lines will be indented
|
||||||
/// by this much more than the first line, if wrapping is enabled in
|
/// by this much more than the first line, if wrapping is enabled in
|
||||||
/// [outputPreferences].
|
/// [outputPreferences].
|
||||||
/// If [wrap] is specified, then it overrides the
|
///
|
||||||
/// [outputPreferences.wrapText] setting.
|
/// If `wrap` is specified, then it overrides the
|
||||||
|
/// `outputPreferences.wrapText` setting.
|
||||||
void printError(
|
void printError(
|
||||||
String message, {
|
String message, {
|
||||||
StackTrace stackTrace,
|
StackTrace stackTrace,
|
||||||
@ -55,24 +63,31 @@ abstract class Logger {
|
|||||||
/// Display normal output of the command. This should be used for things like
|
/// Display normal output of the command. This should be used for things like
|
||||||
/// progress messages, success messages, or just normal command output.
|
/// progress messages, success messages, or just normal command output.
|
||||||
///
|
///
|
||||||
/// The [message] argument is printed to the stderr in red by default.
|
/// The `message` argument is printed to the stderr in red by default.
|
||||||
/// The [stackTrace] argument is the stack trace that will be printed if
|
///
|
||||||
|
/// The `stackTrace` argument is the stack trace that will be printed if
|
||||||
/// supplied.
|
/// supplied.
|
||||||
/// If the [emphasis] argument is true, it will cause the output message be
|
///
|
||||||
|
/// If the `emphasis` argument is true, it will cause the output message be
|
||||||
/// printed in bold text. Defaults to false.
|
/// printed in bold text. Defaults to false.
|
||||||
/// The [color] argument will print the message in the supplied color instead
|
///
|
||||||
|
/// The `color` argument will print the message in the supplied color instead
|
||||||
/// of the default of red. Colors will not be printed if the output terminal
|
/// of the default of red. Colors will not be printed if the output terminal
|
||||||
/// doesn't support them.
|
/// doesn't support them.
|
||||||
/// If [newline] is true, then a newline will be added after printing the
|
///
|
||||||
|
/// If `newline` is true, then a newline will be added after printing the
|
||||||
/// status. Defaults to true.
|
/// status. Defaults to true.
|
||||||
/// The [indent] argument specifies the number of spaces to indent the overall
|
///
|
||||||
|
/// The `indent` argument specifies the number of spaces to indent the overall
|
||||||
/// message. If wrapping is enabled in [outputPreferences], then the wrapped
|
/// message. If wrapping is enabled in [outputPreferences], then the wrapped
|
||||||
/// lines will be indented as well.
|
/// lines will be indented as well.
|
||||||
/// If [hangingIndent] is specified, then any wrapped lines will be indented
|
///
|
||||||
|
/// If `hangingIndent` is specified, then any wrapped lines will be indented
|
||||||
/// by this much more than the first line, if wrapping is enabled in
|
/// by this much more than the first line, if wrapping is enabled in
|
||||||
/// [outputPreferences].
|
/// [outputPreferences].
|
||||||
/// If [wrap] is specified, then it overrides the
|
///
|
||||||
/// [outputPreferences.wrapText] setting.
|
/// If `wrap` is specified, then it overrides the
|
||||||
|
/// `outputPreferences.wrapText` setting.
|
||||||
void printStatus(
|
void printStatus(
|
||||||
String message, {
|
String message, {
|
||||||
bool emphasis,
|
bool emphasis,
|
||||||
@ -89,17 +104,25 @@ abstract class Logger {
|
|||||||
|
|
||||||
/// Start an indeterminate progress display.
|
/// Start an indeterminate progress display.
|
||||||
///
|
///
|
||||||
/// [message] is the message to display to the user; [progressId] provides an ID which can be
|
/// The `message` argument is the message to display to the user.
|
||||||
/// used to identify this type of progress (`hot.reload`, `hot.restart`, ...).
|
|
||||||
///
|
///
|
||||||
/// [progressIndicatorPadding] can optionally be used to specify spacing
|
/// The `timeout` argument sets a duration after which an additional message
|
||||||
/// between the [message] and the progress indicator.
|
/// may be shown saying that the operation is taking a long time. (Not all
|
||||||
|
/// [Status] subclasses show such a message.) Set this to null if the
|
||||||
|
/// operation can legitimately take an abritrary amount of time (e.g. waiting
|
||||||
|
/// for the user).
|
||||||
|
///
|
||||||
|
/// The `progressId` argument provides an ID that can be used to identify
|
||||||
|
/// this type of progress (e.g. `hot.reload`, `hot.restart`).
|
||||||
|
///
|
||||||
|
/// The `progressIndicatorPadding` can optionally be used to specify spacing
|
||||||
|
/// between the `message` and the progress indicator, if any.
|
||||||
Status startProgress(
|
Status startProgress(
|
||||||
String message, {
|
String message, {
|
||||||
|
@required Duration timeout,
|
||||||
String progressId,
|
String progressId,
|
||||||
bool expectSlowOperation,
|
bool multilineOutput = false,
|
||||||
bool multilineOutput,
|
int progressIndicatorPadding = kDefaultStatusPadding,
|
||||||
int progressIndicatorPadding,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,17 +142,16 @@ class StdoutLogger extends Logger {
|
|||||||
int hangingIndent,
|
int hangingIndent,
|
||||||
bool wrap,
|
bool wrap,
|
||||||
}) {
|
}) {
|
||||||
|
_status?.pause();
|
||||||
message ??= '';
|
message ??= '';
|
||||||
message = wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap);
|
message = wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap);
|
||||||
_status?.cancel();
|
|
||||||
_status = null;
|
|
||||||
if (emphasis == true)
|
if (emphasis == true)
|
||||||
message = terminal.bolden(message);
|
message = terminal.bolden(message);
|
||||||
message = terminal.color(message, color ?? TerminalColor.red);
|
message = terminal.color(message, color ?? TerminalColor.red);
|
||||||
stderr.writeln(message);
|
stderr.writeln(message);
|
||||||
if (stackTrace != null) {
|
if (stackTrace != null)
|
||||||
stderr.writeln(stackTrace.toString());
|
stderr.writeln(stackTrace.toString());
|
||||||
}
|
_status?.resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -142,10 +164,9 @@ class StdoutLogger extends Logger {
|
|||||||
int hangingIndent,
|
int hangingIndent,
|
||||||
bool wrap,
|
bool wrap,
|
||||||
}) {
|
}) {
|
||||||
|
_status?.pause();
|
||||||
message ??= '';
|
message ??= '';
|
||||||
message = wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap);
|
message = wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap);
|
||||||
_status?.cancel();
|
|
||||||
_status = null;
|
|
||||||
if (emphasis == true)
|
if (emphasis == true)
|
||||||
message = terminal.bolden(message);
|
message = terminal.bolden(message);
|
||||||
if (color != null)
|
if (color != null)
|
||||||
@ -153,6 +174,7 @@ class StdoutLogger extends Logger {
|
|||||||
if (newline != false)
|
if (newline != false)
|
||||||
message = '$message\n';
|
message = '$message\n';
|
||||||
writeToStdOut(message);
|
writeToStdOut(message);
|
||||||
|
_status?.resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
@ -166,21 +188,23 @@ class StdoutLogger extends Logger {
|
|||||||
@override
|
@override
|
||||||
Status startProgress(
|
Status startProgress(
|
||||||
String message, {
|
String message, {
|
||||||
|
@required Duration timeout,
|
||||||
String progressId,
|
String progressId,
|
||||||
bool expectSlowOperation,
|
bool multilineOutput = false,
|
||||||
bool multilineOutput,
|
int progressIndicatorPadding = kDefaultStatusPadding,
|
||||||
int progressIndicatorPadding,
|
|
||||||
}) {
|
}) {
|
||||||
expectSlowOperation ??= false;
|
assert(progressIndicatorPadding != null);
|
||||||
progressIndicatorPadding ??= kDefaultStatusPadding;
|
|
||||||
if (_status != null) {
|
if (_status != null) {
|
||||||
// Ignore nested progresses; return a no-op status object.
|
// Ignore nested progresses; return a no-op status object.
|
||||||
return Status(onFinish: _clearStatus)..start();
|
return SilentStatus(
|
||||||
|
timeout: timeout,
|
||||||
|
onFinish: _clearStatus,
|
||||||
|
)..start();
|
||||||
}
|
}
|
||||||
if (terminal.supportsColor) {
|
if (terminal.supportsColor) {
|
||||||
_status = AnsiStatus(
|
_status = AnsiStatus(
|
||||||
message: message,
|
message: message,
|
||||||
expectSlowOperation: expectSlowOperation,
|
timeout: timeout,
|
||||||
multilineOutput: multilineOutput,
|
multilineOutput: multilineOutput,
|
||||||
padding: progressIndicatorPadding,
|
padding: progressIndicatorPadding,
|
||||||
onFinish: _clearStatus,
|
onFinish: _clearStatus,
|
||||||
@ -188,7 +212,7 @@ class StdoutLogger extends Logger {
|
|||||||
} else {
|
} else {
|
||||||
_status = SummaryStatus(
|
_status = SummaryStatus(
|
||||||
message: message,
|
message: message,
|
||||||
expectSlowOperation: expectSlowOperation,
|
timeout: timeout,
|
||||||
padding: progressIndicatorPadding,
|
padding: progressIndicatorPadding,
|
||||||
onFinish: _clearStatus,
|
onFinish: _clearStatus,
|
||||||
)..start();
|
)..start();
|
||||||
@ -270,13 +294,14 @@ class BufferLogger extends Logger {
|
|||||||
@override
|
@override
|
||||||
Status startProgress(
|
Status startProgress(
|
||||||
String message, {
|
String message, {
|
||||||
|
@required Duration timeout,
|
||||||
String progressId,
|
String progressId,
|
||||||
bool expectSlowOperation,
|
bool multilineOutput = false,
|
||||||
bool multilineOutput,
|
int progressIndicatorPadding = kDefaultStatusPadding,
|
||||||
int progressIndicatorPadding,
|
|
||||||
}) {
|
}) {
|
||||||
|
assert(progressIndicatorPadding != null);
|
||||||
printStatus(message);
|
printStatus(message);
|
||||||
return Status()..start();
|
return SilentStatus(timeout: timeout)..start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears all buffers.
|
/// Clears all buffers.
|
||||||
@ -337,15 +362,30 @@ class VerboseLogger extends Logger {
|
|||||||
@override
|
@override
|
||||||
Status startProgress(
|
Status startProgress(
|
||||||
String message, {
|
String message, {
|
||||||
|
@required Duration timeout,
|
||||||
String progressId,
|
String progressId,
|
||||||
bool expectSlowOperation,
|
bool multilineOutput = false,
|
||||||
bool multilineOutput,
|
int progressIndicatorPadding = kDefaultStatusPadding,
|
||||||
int progressIndicatorPadding,
|
|
||||||
}) {
|
}) {
|
||||||
|
assert(progressIndicatorPadding != null);
|
||||||
printStatus(message);
|
printStatus(message);
|
||||||
return Status(onFinish: () {
|
final Stopwatch timer = Stopwatch()..start();
|
||||||
printTrace('$message (completed)');
|
return SilentStatus(
|
||||||
})..start();
|
timeout: timeout,
|
||||||
|
onFinish: () {
|
||||||
|
String time;
|
||||||
|
if (timeout == null || timeout > kFastOperation) {
|
||||||
|
time = getElapsedAsSeconds(timer.elapsed);
|
||||||
|
} else {
|
||||||
|
time = getElapsedAsMilliseconds(timer.elapsed);
|
||||||
|
}
|
||||||
|
if (timeout != null && timer.elapsed > timeout) {
|
||||||
|
printTrace('$message (completed in $time, longer than expected)');
|
||||||
|
} else {
|
||||||
|
printTrace('$message (completed in $time)');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)..start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _emit(_LogType type, String message, [StackTrace stackTrace]) {
|
void _emit(_LogType type, String message, [StackTrace stackTrace]) {
|
||||||
@ -383,10 +423,15 @@ class VerboseLogger extends Logger {
|
|||||||
|
|
||||||
enum _LogType { error, status, trace }
|
enum _LogType { error, status, trace }
|
||||||
|
|
||||||
|
typedef SlowWarningCallback = String Function();
|
||||||
|
|
||||||
/// A [Status] class begins when start is called, and may produce progress
|
/// A [Status] class begins when start is called, and may produce progress
|
||||||
/// information asynchronously.
|
/// information asynchronously.
|
||||||
///
|
///
|
||||||
/// The [Status] class itself never has any output.
|
/// Some subclasses change output once [timeout] has expired, to indicate that
|
||||||
|
/// something is taking longer than expected.
|
||||||
|
///
|
||||||
|
/// The [SilentStatus] class never has any output.
|
||||||
///
|
///
|
||||||
/// The [AnsiSpinner] subclass shows a spinner, and replaces it with a single
|
/// The [AnsiSpinner] subclass shows a spinner, and replaces it with a single
|
||||||
/// space character when stopped or canceled.
|
/// space character when stopped or canceled.
|
||||||
@ -395,51 +440,159 @@ enum _LogType { error, status, trace }
|
|||||||
/// information when stopped. When canceled, the information isn't shown. In
|
/// information when stopped. When canceled, the information isn't shown. In
|
||||||
/// either case, a newline is printed.
|
/// either case, a newline is printed.
|
||||||
///
|
///
|
||||||
|
/// The [SummaryStatus] subclass shows only a static message (without an
|
||||||
|
/// indicator), then updates it when the operation ends.
|
||||||
|
///
|
||||||
/// Generally, consider `logger.startProgress` instead of directly creating
|
/// Generally, consider `logger.startProgress` instead of directly creating
|
||||||
/// a [Status] or one of its subclasses.
|
/// a [Status] or one of its subclasses.
|
||||||
class Status {
|
abstract class Status {
|
||||||
Status({this.onFinish});
|
Status({ @required this.timeout, this.onFinish });
|
||||||
|
|
||||||
/// A straight [Status] or an [AnsiSpinner] (depending on whether the
|
/// A [SilentStatus] or an [AnsiSpinner] (depending on whether the
|
||||||
/// terminal is fancy enough), already started.
|
/// terminal is fancy enough), already started.
|
||||||
factory Status.withSpinner({ VoidCallback onFinish }) {
|
factory Status.withSpinner({
|
||||||
|
@required Duration timeout,
|
||||||
|
VoidCallback onFinish,
|
||||||
|
SlowWarningCallback slowWarningCallback,
|
||||||
|
}) {
|
||||||
if (terminal.supportsColor)
|
if (terminal.supportsColor)
|
||||||
return AnsiSpinner(onFinish: onFinish)..start();
|
return AnsiSpinner(timeout: timeout, onFinish: onFinish, slowWarningCallback: slowWarningCallback)..start();
|
||||||
return Status(onFinish: onFinish)..start();
|
return SilentStatus(timeout: timeout, onFinish: onFinish)..start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Duration timeout;
|
||||||
final VoidCallback onFinish;
|
final VoidCallback onFinish;
|
||||||
|
|
||||||
bool _isStarted = false;
|
@protected
|
||||||
|
final Stopwatch _stopwatch = Stopwatch();
|
||||||
|
|
||||||
|
@protected
|
||||||
|
bool get seemsSlow => timeout != null && _stopwatch.elapsed > timeout;
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String get elapsedTime {
|
||||||
|
if (timeout == null || timeout > kFastOperation)
|
||||||
|
return getElapsedAsSeconds(_stopwatch.elapsed);
|
||||||
|
return getElapsedAsMilliseconds(_stopwatch.elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
/// Call to start spinning.
|
/// Call to start spinning.
|
||||||
void start() {
|
void start() {
|
||||||
assert(!_isStarted);
|
assert(!_stopwatch.isRunning);
|
||||||
_isStarted = true;
|
_stopwatch.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Call to stop spinning after success.
|
/// Call to stop spinning after success.
|
||||||
void stop() {
|
void stop() {
|
||||||
assert(_isStarted);
|
finish();
|
||||||
_isStarted = false;
|
|
||||||
if (onFinish != null)
|
|
||||||
onFinish();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Call to cancel the spinner after failure or cancellation.
|
/// Call to cancel the spinner after failure or cancellation.
|
||||||
void cancel() {
|
void cancel() {
|
||||||
assert(_isStarted);
|
finish();
|
||||||
_isStarted = false;
|
}
|
||||||
|
|
||||||
|
/// Call to clear the current line but not end the progress.
|
||||||
|
void pause() { }
|
||||||
|
|
||||||
|
/// Call to resume after a pause.
|
||||||
|
void resume() { }
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void finish() {
|
||||||
|
assert(_stopwatch.isRunning);
|
||||||
|
_stopwatch.stop();
|
||||||
if (onFinish != null)
|
if (onFinish != null)
|
||||||
onFinish();
|
onFinish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A [SilentStatus] shows nothing.
|
||||||
|
class SilentStatus extends Status {
|
||||||
|
SilentStatus({
|
||||||
|
@required Duration timeout,
|
||||||
|
VoidCallback onFinish,
|
||||||
|
}) : super(timeout: timeout, onFinish: onFinish);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructor writes [message] to [stdout]. On [cancel] or [stop], will call
|
||||||
|
/// [onFinish]. On [stop], will additionally print out summary information.
|
||||||
|
class SummaryStatus extends Status {
|
||||||
|
SummaryStatus({
|
||||||
|
this.message = '',
|
||||||
|
@required Duration timeout,
|
||||||
|
this.padding = kDefaultStatusPadding,
|
||||||
|
VoidCallback onFinish,
|
||||||
|
}) : assert(message != null),
|
||||||
|
assert(padding != null),
|
||||||
|
super(timeout: timeout, onFinish: onFinish);
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
final int padding;
|
||||||
|
|
||||||
|
bool _messageShowingOnCurrentLine = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void start() {
|
||||||
|
_printMessage();
|
||||||
|
super.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _printMessage() {
|
||||||
|
assert(!_messageShowingOnCurrentLine);
|
||||||
|
stdout.write('${message.padRight(padding)} ');
|
||||||
|
_messageShowingOnCurrentLine = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void stop() {
|
||||||
|
if (!_messageShowingOnCurrentLine)
|
||||||
|
_printMessage();
|
||||||
|
super.stop();
|
||||||
|
writeSummaryInformation();
|
||||||
|
stdout.write('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void cancel() {
|
||||||
|
super.cancel();
|
||||||
|
if (_messageShowingOnCurrentLine)
|
||||||
|
stdout.write('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints a (minimum) 8 character padded time.
|
||||||
|
///
|
||||||
|
/// If [timeout] is less than or equal to [kFastOperation], the time is in
|
||||||
|
/// seconds; otherwise, milliseconds. If the time is longer than [timeout],
|
||||||
|
/// appends "(!)" to the time.
|
||||||
|
///
|
||||||
|
/// Examples: ` 0.5s`, ` 150ms`, ` 1,600ms`, ` 3.1s (!)`
|
||||||
|
void writeSummaryInformation() {
|
||||||
|
assert(_messageShowingOnCurrentLine);
|
||||||
|
stdout.write(elapsedTime.padLeft(_kTimePadding));
|
||||||
|
if (seemsSlow)
|
||||||
|
stdout.write(' (!)');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void pause() {
|
||||||
|
super.pause();
|
||||||
|
stdout.write('\n');
|
||||||
|
_messageShowingOnCurrentLine = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An [AnsiSpinner] is a simple animation that does nothing but implement a
|
/// An [AnsiSpinner] is a simple animation that does nothing but implement a
|
||||||
/// ASCII/Unicode spinner. When stopped or canceled, the animation erases
|
/// terminal spinner. When stopped or canceled, the animation erases itself.
|
||||||
/// itself.
|
///
|
||||||
|
/// If the timeout expires, a customizable warning is shown (but the spinner
|
||||||
|
/// continues otherwise unabated).
|
||||||
class AnsiSpinner extends Status {
|
class AnsiSpinner extends Status {
|
||||||
AnsiSpinner({VoidCallback onFinish}) : super(onFinish: onFinish);
|
AnsiSpinner({
|
||||||
|
@required Duration timeout,
|
||||||
|
VoidCallback onFinish,
|
||||||
|
this.slowWarningCallback,
|
||||||
|
}) : super(timeout: timeout, onFinish: onFinish);
|
||||||
|
|
||||||
int ticks = 0;
|
int ticks = 0;
|
||||||
Timer timer;
|
Timer timer;
|
||||||
@ -449,73 +602,123 @@ class AnsiSpinner extends Status {
|
|||||||
? <String>[r'-', r'\', r'|', r'/']
|
? <String>[r'-', r'\', r'|', r'/']
|
||||||
: <String>['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
|
: <String>['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
|
||||||
|
|
||||||
String get _backspace => '\b' * _animation[0].length;
|
static const String _defaultSlowWarning = '(This is taking an unexpectedly long time.)';
|
||||||
String get _clear => ' ' * _animation[0].length;
|
final SlowWarningCallback slowWarningCallback;
|
||||||
|
|
||||||
void _callback(Timer timer) {
|
String _slowWarning = '';
|
||||||
stdout.write('$_backspace${_animation[ticks++ % _animation.length]}');
|
|
||||||
}
|
String get _currentAnimationFrame => _animation[ticks % _animation.length];
|
||||||
|
int get _currentLength => _currentAnimationFrame.length + _slowWarning.length;
|
||||||
|
String get _backspace => '\b' * _currentLength;
|
||||||
|
String get _clear => ' ' * _currentLength;
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int get spinnerIndent => 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void start() {
|
void start() {
|
||||||
super.start();
|
super.start();
|
||||||
assert(timer == null);
|
assert(timer == null);
|
||||||
stdout.write(' ');
|
_startSpinner();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startSpinner() {
|
||||||
|
stdout.write(_clear * (spinnerIndent + 1)); // for _callback to backspace over
|
||||||
timer = Timer.periodic(const Duration(milliseconds: 100), _callback);
|
timer = Timer.periodic(const Duration(milliseconds: 100), _callback);
|
||||||
_callback(timer);
|
_callback(timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _callback(Timer timer) {
|
||||||
void stop() {
|
assert(this.timer == timer);
|
||||||
|
assert(timer != null);
|
||||||
assert(timer.isActive);
|
assert(timer.isActive);
|
||||||
timer.cancel();
|
stdout.write('${_backspace * (spinnerIndent + 1)}');
|
||||||
stdout.write('$_backspace$_clear$_backspace');
|
ticks += 1;
|
||||||
super.stop();
|
stdout.write('${_clear * spinnerIndent}$_currentAnimationFrame');
|
||||||
|
if (seemsSlow) {
|
||||||
|
if (slowWarningCallback != null) {
|
||||||
|
_slowWarning = ' ' + slowWarningCallback();
|
||||||
|
} else {
|
||||||
|
_slowWarning = ' ' + _defaultSlowWarning;
|
||||||
|
}
|
||||||
|
stdout.write(_slowWarning);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void cancel() {
|
void finish() {
|
||||||
|
assert(timer != null);
|
||||||
assert(timer.isActive);
|
assert(timer.isActive);
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
stdout.write('$_backspace$_clear$_backspace');
|
timer = null;
|
||||||
super.cancel();
|
_clearSpinner();
|
||||||
|
super.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearSpinner() {
|
||||||
|
final int width = spinnerIndent + 1;
|
||||||
|
stdout.write('${_backspace * width}${_clear * width}${_backspace * width}');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void pause() {
|
||||||
|
assert(timer != null);
|
||||||
|
assert(timer.isActive);
|
||||||
|
_clearSpinner();
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void resume() {
|
||||||
|
assert(timer != null);
|
||||||
|
assert(!timer.isActive);
|
||||||
|
_startSpinner();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructor writes [message] to [stdout] with padding, then starts as an
|
const int _kTimePadding = 8; // should fit "99,999ms"
|
||||||
/// [AnsiSpinner]. On [cancel] or [stop], will call [onFinish].
|
|
||||||
/// On [stop], will additionally print out summary information in
|
/// Constructor writes [message] to [stdout] with padding, then starts an
|
||||||
/// milliseconds if [expectSlowOperation] is false, as seconds otherwise.
|
/// indeterminate progress indicator animation (it's a subclass of
|
||||||
|
/// [AnsiSpinner]).
|
||||||
|
///
|
||||||
|
/// On [cancel] or [stop], will call [onFinish]. On [stop], will
|
||||||
|
/// additionally print out summary information.
|
||||||
class AnsiStatus extends AnsiSpinner {
|
class AnsiStatus extends AnsiSpinner {
|
||||||
AnsiStatus({
|
AnsiStatus({
|
||||||
String message,
|
this.message = '',
|
||||||
bool expectSlowOperation,
|
@required Duration timeout,
|
||||||
bool multilineOutput,
|
this.multilineOutput = false,
|
||||||
int padding,
|
this.padding = kDefaultStatusPadding,
|
||||||
VoidCallback onFinish,
|
VoidCallback onFinish,
|
||||||
}) : message = message ?? '',
|
}) : assert(message != null),
|
||||||
padding = padding ?? 0,
|
assert(multilineOutput != null),
|
||||||
expectSlowOperation = expectSlowOperation ?? false,
|
assert(padding != null),
|
||||||
multilineOutput = multilineOutput ?? false,
|
super(timeout: timeout, onFinish: onFinish);
|
||||||
super(onFinish: onFinish);
|
|
||||||
|
|
||||||
final String message;
|
final String message;
|
||||||
final bool expectSlowOperation;
|
|
||||||
final bool multilineOutput;
|
final bool multilineOutput;
|
||||||
final int padding;
|
final int padding;
|
||||||
|
|
||||||
Stopwatch stopwatch;
|
|
||||||
|
|
||||||
static const String _margin = ' ';
|
static const String _margin = ' ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get spinnerIndent => _kTimePadding - 1;
|
||||||
|
|
||||||
|
int _totalMessageLength;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void start() {
|
void start() {
|
||||||
assert(stopwatch == null || !stopwatch.isRunning);
|
_startStatus();
|
||||||
stopwatch = Stopwatch()..start();
|
|
||||||
stdout.write('${message.padRight(padding)}$_margin');
|
|
||||||
super.start();
|
super.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _startStatus() {
|
||||||
|
final String line = '${message.padRight(padding)}$_margin';
|
||||||
|
_totalMessageLength = line.length;
|
||||||
|
stdout.write(line);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void stop() {
|
void stop() {
|
||||||
super.stop();
|
super.stop();
|
||||||
@ -531,74 +734,31 @@ class AnsiStatus extends AnsiSpinner {
|
|||||||
|
|
||||||
/// Print summary information when a task is done.
|
/// Print summary information when a task is done.
|
||||||
///
|
///
|
||||||
/// If [multilineOutput] is false, backs up 4 characters and prints a
|
/// If [multilineOutput] is false, replaces the spinner with the summary message.
|
||||||
/// (minimum) 5 character padded time. If [expectSlowOperation] is true, the
|
|
||||||
/// time is in seconds; otherwise, milliseconds. Only backs up 4 characters
|
|
||||||
/// because [super.cancel] backs up one.
|
|
||||||
///
|
///
|
||||||
/// If [multilineOutput] is true, then it prints the message again on a new
|
/// If [multilineOutput] is true, then it prints the message again on a new
|
||||||
/// line before writing the elapsed time, and doesn't back up at all.
|
/// line before writing the elapsed time.
|
||||||
void writeSummaryInformation() {
|
void writeSummaryInformation() {
|
||||||
final String prefix = multilineOutput
|
if (multilineOutput)
|
||||||
? '\n${'$message Done'.padRight(padding - 4)}$_margin'
|
stdout.write('\n${'$message Done'.padRight(padding)}$_margin');
|
||||||
: '\b\b\b\b';
|
stdout.write(elapsedTime.padLeft(_kTimePadding));
|
||||||
if (expectSlowOperation) {
|
if (seemsSlow)
|
||||||
stdout.write('$prefix${getElapsedAsSeconds(stopwatch.elapsed).padLeft(5)}');
|
stdout.write(' (!)');
|
||||||
} else {
|
|
||||||
stdout.write('$prefix${getElapsedAsMilliseconds(stopwatch.elapsed).padLeft(5)}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructor writes [message] to [stdout]. On [cancel] or [stop], will call
|
|
||||||
/// [onFinish]. On [stop], will additionally print out summary information in
|
|
||||||
/// milliseconds if [expectSlowOperation] is false, as seconds otherwise.
|
|
||||||
class SummaryStatus extends Status {
|
|
||||||
SummaryStatus({
|
|
||||||
String message,
|
|
||||||
bool expectSlowOperation,
|
|
||||||
int padding,
|
|
||||||
VoidCallback onFinish,
|
|
||||||
}) : message = message ?? '',
|
|
||||||
padding = padding ?? 0,
|
|
||||||
expectSlowOperation = expectSlowOperation ?? false,
|
|
||||||
super(onFinish: onFinish);
|
|
||||||
|
|
||||||
final String message;
|
|
||||||
final bool expectSlowOperation;
|
|
||||||
final int padding;
|
|
||||||
|
|
||||||
Stopwatch stopwatch;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void start() {
|
|
||||||
stopwatch = Stopwatch()..start();
|
|
||||||
stdout.write('${message.padRight(padding)} ');
|
|
||||||
super.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void stop() {
|
|
||||||
super.stop();
|
|
||||||
writeSummaryInformation();
|
|
||||||
stdout.write('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void cancel() {
|
|
||||||
super.cancel();
|
|
||||||
stdout.write('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prints a (minimum) 5 character padded time. If [expectSlowOperation] is
|
|
||||||
/// true, the time is in seconds; otherwise, milliseconds.
|
|
||||||
///
|
|
||||||
/// Example: ' 0.5s', '150ms', '1600ms'
|
|
||||||
void writeSummaryInformation() {
|
|
||||||
if (expectSlowOperation) {
|
|
||||||
stdout.write(getElapsedAsSeconds(stopwatch.elapsed).padLeft(5));
|
|
||||||
} else {
|
|
||||||
stdout.write(getElapsedAsMilliseconds(stopwatch.elapsed).padLeft(5));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _clearStatus() {
|
||||||
|
stdout.write('${_backspace * _totalMessageLength}${_clear * _totalMessageLength}${_backspace * _totalMessageLength}');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void pause() {
|
||||||
|
super.pause();
|
||||||
|
_clearStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void resume() {
|
||||||
|
_startStatus();
|
||||||
|
super.resume();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ Future<List<int>> _attempt(Uri url, {bool onlyHeaders = false}) async {
|
|||||||
printTrace('Downloading: $url');
|
printTrace('Downloading: $url');
|
||||||
HttpClient httpClient;
|
HttpClient httpClient;
|
||||||
if (context[HttpClientFactory] != null) {
|
if (context[HttpClientFactory] != null) {
|
||||||
httpClient = context[HttpClientFactory]();
|
httpClient = (context[HttpClientFactory] as HttpClientFactory)(); // ignore: avoid_as
|
||||||
} else {
|
} else {
|
||||||
httpClient = HttpClient();
|
httpClient = HttpClient();
|
||||||
}
|
}
|
||||||
|
@ -203,14 +203,6 @@ Future<int> runInteractively(List<String> command, {
|
|||||||
return await process.exitCode;
|
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) {
|
Future<Process> runDetached(List<String> cmd) {
|
||||||
_traceCommand(cmd);
|
_traceCommand(cmd);
|
||||||
final Future<Process> proc = processManager.start(
|
final Future<Process> proc = processManager.start(
|
||||||
|
@ -5,8 +5,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert' show AsciiDecoder;
|
import 'dart:convert' show AsciiDecoder;
|
||||||
|
|
||||||
import 'package:quiver/strings.dart';
|
|
||||||
|
|
||||||
import '../globals.dart';
|
import '../globals.dart';
|
||||||
import 'context.dart';
|
import 'context.dart';
|
||||||
import 'io.dart' as io;
|
import 'io.dart' as io;
|
||||||
@ -172,31 +170,32 @@ class AnsiTerminal {
|
|||||||
/// Return keystrokes from the console.
|
/// Return keystrokes from the console.
|
||||||
///
|
///
|
||||||
/// Useful when the console is in [singleCharMode].
|
/// 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();
|
_broadcastStdInString ??= io.stdin.transform<String>(const AsciiDecoder(allowInvalid: true)).asBroadcastStream();
|
||||||
return _broadcastStdInString;
|
return _broadcastStdInString;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prompts the user to input a character within the accepted list. Re-prompts
|
/// Prompts the user to input a character within a given list. Re-prompts if
|
||||||
/// if entered character is not in the list.
|
/// entered character is not in the list.
|
||||||
///
|
///
|
||||||
/// The [prompt] is the text displayed prior to waiting for user input. The
|
/// The `prompt`, if non-null, is the text displayed prior to waiting for user
|
||||||
/// [defaultChoiceIndex], if given, will be the character appearing in
|
/// input each time. If `prompt` is non-null and `displayAcceptedCharacters`
|
||||||
/// [acceptedCharacters] in the index given if the user presses enter without
|
/// is true, the accepted keys are printed next to the `prompt`.
|
||||||
/// any key input. Setting [displayAcceptedCharacters] also prints the
|
|
||||||
/// accepted keys next to the [prompt].
|
|
||||||
///
|
///
|
||||||
/// Throws a [TimeoutException] if a `timeout` is provided and its duration
|
/// The returned value is the user's input; if `defaultChoiceIndex` is not
|
||||||
/// expired without user input. Duration resets per key press.
|
/// 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(
|
Future<String> promptForCharInput(
|
||||||
List<String> acceptedCharacters, {
|
List<String> acceptedCharacters, {
|
||||||
String prompt,
|
String prompt,
|
||||||
int defaultChoiceIndex,
|
int defaultChoiceIndex,
|
||||||
bool displayAcceptedCharacters = true,
|
bool displayAcceptedCharacters = true,
|
||||||
Duration timeout,
|
|
||||||
}) async {
|
}) async {
|
||||||
assert(acceptedCharacters != null);
|
assert(acceptedCharacters != null);
|
||||||
assert(acceptedCharacters.isNotEmpty);
|
assert(acceptedCharacters.isNotEmpty);
|
||||||
|
assert(prompt == null || prompt.isNotEmpty);
|
||||||
|
assert(displayAcceptedCharacters != null);
|
||||||
List<String> charactersToDisplay = acceptedCharacters;
|
List<String> charactersToDisplay = acceptedCharacters;
|
||||||
if (defaultChoiceIndex != null) {
|
if (defaultChoiceIndex != null) {
|
||||||
assert(defaultChoiceIndex >= 0 && defaultChoiceIndex < acceptedCharacters.length);
|
assert(defaultChoiceIndex >= 0 && defaultChoiceIndex < acceptedCharacters.length);
|
||||||
@ -206,17 +205,14 @@ class AnsiTerminal {
|
|||||||
}
|
}
|
||||||
String choice;
|
String choice;
|
||||||
singleCharMode = true;
|
singleCharMode = true;
|
||||||
while (isEmpty(choice) || choice.length != 1 || !acceptedCharacters.contains(choice)) {
|
while (choice == null || choice.length > 1 || !acceptedCharacters.contains(choice)) {
|
||||||
if (isNotEmpty(prompt)) {
|
if (prompt != null) {
|
||||||
printStatus(prompt, emphasis: true, newline: false);
|
printStatus(prompt, emphasis: true, newline: false);
|
||||||
if (displayAcceptedCharacters)
|
if (displayAcceptedCharacters)
|
||||||
printStatus(' [${charactersToDisplay.join("|")}]', newline: false);
|
printStatus(' [${charactersToDisplay.join("|")}]', newline: false);
|
||||||
printStatus(': ', emphasis: true, newline: false);
|
printStatus(': ', emphasis: true, newline: false);
|
||||||
}
|
}
|
||||||
Future<String> inputFuture = onCharInput.first;
|
choice = await keystrokes.first;
|
||||||
if (timeout != null)
|
|
||||||
inputFuture = inputFuture.timeout(timeout);
|
|
||||||
choice = await inputFuture;
|
|
||||||
printStatus(choice);
|
printStatus(choice);
|
||||||
}
|
}
|
||||||
singleCharMode = false;
|
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)) {
|
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 {
|
return _withDownloadFile('${flattenNameSubdirs(url)}', (File tempFile) async {
|
||||||
if (!verifier(tempFile)) {
|
if (!verifier(tempFile)) {
|
||||||
final Status status = logger.startProgress(message, expectSlowOperation: true);
|
final Status status = logger.startProgress(message, timeout: kSlowOperation);
|
||||||
try {
|
try {
|
||||||
await _downloadFile(url, tempFile);
|
await _downloadFile(url, tempFile);
|
||||||
status.stop();
|
status.stop();
|
||||||
@ -648,7 +648,7 @@ Future<void> _downloadFile(Uri url, File location) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _doesRemoteExist(String message, Uri url) 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);
|
final bool exists = await doesRemoteFileExist(url);
|
||||||
status.stop();
|
status.stop();
|
||||||
return exists;
|
return exists;
|
||||||
|
@ -74,11 +74,12 @@ class AnalyzeContinuously extends AnalyzeBase {
|
|||||||
analysisStatus?.cancel();
|
analysisStatus?.cancel();
|
||||||
if (!firstAnalysis)
|
if (!firstAnalysis)
|
||||||
printStatus('\n');
|
printStatus('\n');
|
||||||
analysisStatus = logger.startProgress('Analyzing $analysisTarget...');
|
analysisStatus = logger.startProgress('Analyzing $analysisTarget...', timeout: kSlowOperation);
|
||||||
analyzedPaths.clear();
|
analyzedPaths.clear();
|
||||||
analysisTimer = Stopwatch()..start();
|
analysisTimer = Stopwatch()..start();
|
||||||
} else {
|
} else {
|
||||||
analysisStatus?.stop();
|
analysisStatus?.stop();
|
||||||
|
analysisStatus = null;
|
||||||
analysisTimer.stop();
|
analysisTimer.stop();
|
||||||
|
|
||||||
logger.printStatus(terminal.clearScreen(), newline: false);
|
logger.printStatus(terminal.clearScreen(), newline: false);
|
||||||
|
@ -107,7 +107,7 @@ class AnalyzeOnce extends AnalyzeBase {
|
|||||||
? '${directories.length} ${directories.length == 1 ? 'directory' : 'directories'}'
|
? '${directories.length} ${directories.length == 1 ? 'directory' : 'directories'}'
|
||||||
: fs.path.basename(directories.first);
|
: fs.path.basename(directories.first);
|
||||||
final Status progress = argResults['preamble']
|
final Status progress = argResults['preamble']
|
||||||
? logger.startProgress('Analyzing $message...')
|
? logger.startProgress('Analyzing $message...', timeout: kSlowOperation)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
await analysisCompleter.future;
|
await analysisCompleter.future;
|
||||||
|
@ -135,15 +135,14 @@ class AttachCommand extends FlutterCommand {
|
|||||||
if (device is FuchsiaDevice) {
|
if (device is FuchsiaDevice) {
|
||||||
attachLogger = true;
|
attachLogger = true;
|
||||||
final String module = argResults['module'];
|
final String module = argResults['module'];
|
||||||
if (module == null) {
|
if (module == null)
|
||||||
throwToolExit('\'--module\' is requried for attaching to a Fuchsia device');
|
throwToolExit('\'--module\' is required for attaching to a Fuchsia device');
|
||||||
}
|
|
||||||
usesIpv6 = device.ipv6;
|
usesIpv6 = device.ipv6;
|
||||||
FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
|
FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
|
||||||
try {
|
try {
|
||||||
isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
|
isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
|
||||||
observatoryUri = await isolateDiscoveryProtocol.uri;
|
observatoryUri = await isolateDiscoveryProtocol.uri;
|
||||||
printStatus('Done.');
|
printStatus('Done.'); // FYI, this message is used as a sentinel in tests.
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
isolateDiscoveryProtocol?.dispose();
|
isolateDiscoveryProtocol?.dispose();
|
||||||
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
|
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
|
||||||
@ -163,7 +162,7 @@ class AttachCommand extends FlutterCommand {
|
|||||||
observatoryUri = await observatoryDiscovery.uri;
|
observatoryUri = await observatoryDiscovery.uri;
|
||||||
// Determine ipv6 status from the scanned logs.
|
// Determine ipv6 status from the scanned logs.
|
||||||
usesIpv6 = observatoryDiscovery.ipv6;
|
usesIpv6 = observatoryDiscovery.ipv6;
|
||||||
printStatus('Done.');
|
printStatus('Done.'); // FYI, this message is used as a sentinel in tests.
|
||||||
} finally {
|
} finally {
|
||||||
await observatoryDiscovery?.cancel();
|
await observatoryDiscovery?.cancel();
|
||||||
}
|
}
|
||||||
@ -210,20 +209,30 @@ class AttachCommand extends FlutterCommand {
|
|||||||
if (attachLogger) {
|
if (attachLogger) {
|
||||||
flutterDevice.startEchoingDeviceLog();
|
flutterDevice.startEchoingDeviceLog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int result;
|
||||||
if (daemon != null) {
|
if (daemon != null) {
|
||||||
AppInstance app;
|
AppInstance app;
|
||||||
try {
|
try {
|
||||||
app = await daemon.appDomain.launch(runner, runner.attach,
|
app = await daemon.appDomain.launch(
|
||||||
device, null, true, fs.currentDirectory);
|
runner,
|
||||||
|
runner.attach,
|
||||||
|
device,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
fs.currentDirectory,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throwToolExit(error.toString());
|
throwToolExit(error.toString());
|
||||||
}
|
}
|
||||||
final int result = await app.runner.waitForAppToFinish();
|
result = await app.runner.waitForAppToFinish();
|
||||||
|
assert(result != null);
|
||||||
|
} else {
|
||||||
|
result = await runner.attach();
|
||||||
|
assert(result != null);
|
||||||
|
}
|
||||||
if (result != 0)
|
if (result != 0)
|
||||||
throwToolExit(null, exitCode: result);
|
throwToolExit(null, exitCode: result);
|
||||||
} else {
|
|
||||||
await runner.attach();
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
|
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
|
||||||
for (ForwardedPort port in ports) {
|
for (ForwardedPort port in ports) {
|
||||||
|
@ -4,11 +4,6 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
|
|
||||||
import '../base/file_system.dart';
|
|
||||||
import '../base/utils.dart';
|
|
||||||
import '../globals.dart';
|
|
||||||
import '../runner/flutter_command.dart';
|
import '../runner/flutter_command.dart';
|
||||||
import 'build_aot.dart';
|
import 'build_aot.dart';
|
||||||
import 'build_apk.dart';
|
import 'build_apk.dart';
|
||||||
@ -41,25 +36,4 @@ abstract class BuildSubCommand extends FlutterCommand {
|
|||||||
BuildSubCommand() {
|
BuildSubCommand() {
|
||||||
requiresPubspecYaml();
|
requiresPubspecYaml();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
@mustCallSuper
|
|
||||||
Future<FlutterCommandResult> runCommand() async {
|
|
||||||
if (isRunningOnBot) {
|
|
||||||
final File dotPackages = fs.file('.packages');
|
|
||||||
printStatus('Contents of .packages:');
|
|
||||||
if (dotPackages.existsSync())
|
|
||||||
printStatus(dotPackages.readAsStringSync());
|
|
||||||
else
|
|
||||||
printError('File not found: ${dotPackages.absolute.path}');
|
|
||||||
|
|
||||||
final File pubspecLock = fs.file('pubspec.lock');
|
|
||||||
printStatus('Contents of pubspec.lock:');
|
|
||||||
if (pubspecLock.existsSync())
|
|
||||||
printStatus(pubspecLock.readAsStringSync());
|
|
||||||
else
|
|
||||||
printError('File not found: ${pubspecLock.absolute.path}');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -57,8 +57,6 @@ class BuildAotCommand extends BuildSubCommand {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FlutterCommandResult> runCommand() async {
|
Future<FlutterCommandResult> runCommand() async {
|
||||||
await super.runCommand();
|
|
||||||
|
|
||||||
final String targetPlatform = argResults['target-platform'];
|
final String targetPlatform = argResults['target-platform'];
|
||||||
final TargetPlatform platform = getTargetPlatformForName(targetPlatform);
|
final TargetPlatform platform = getTargetPlatformForName(targetPlatform);
|
||||||
if (platform == null)
|
if (platform == null)
|
||||||
@ -71,7 +69,7 @@ class BuildAotCommand extends BuildSubCommand {
|
|||||||
final String typeName = artifacts.getEngineType(platform, buildMode);
|
final String typeName = artifacts.getEngineType(platform, buildMode);
|
||||||
status = logger.startProgress(
|
status = logger.startProgress(
|
||||||
'Building AOT snapshot in ${getFriendlyModeName(getBuildMode())} mode ($typeName)...',
|
'Building AOT snapshot in ${getFriendlyModeName(getBuildMode())} mode ($typeName)...',
|
||||||
expectSlowOperation: true,
|
timeout: kSlowOperation,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final String outputPath = argResults['output-dir'] ?? getAotBuildDirectory();
|
final String outputPath = argResults['output-dir'] ?? getAotBuildDirectory();
|
||||||
|
@ -42,7 +42,6 @@ class BuildApkCommand extends BuildSubCommand {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FlutterCommandResult> runCommand() async {
|
Future<FlutterCommandResult> runCommand() async {
|
||||||
await super.runCommand();
|
|
||||||
await buildApk(
|
await buildApk(
|
||||||
project: await FlutterProject.current(),
|
project: await FlutterProject.current(),
|
||||||
target: targetFile,
|
target: targetFile,
|
||||||
|
@ -40,7 +40,6 @@ class BuildAppBundleCommand extends BuildSubCommand {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FlutterCommandResult> runCommand() async {
|
Future<FlutterCommandResult> runCommand() async {
|
||||||
await super.runCommand();
|
|
||||||
await buildAppBundle(
|
await buildAppBundle(
|
||||||
project: await FlutterProject.current(),
|
project: await FlutterProject.current(),
|
||||||
target: targetFile,
|
target: targetFile,
|
||||||
|
@ -65,8 +65,6 @@ class BuildBundleCommand extends BuildSubCommand {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FlutterCommandResult> runCommand() async {
|
Future<FlutterCommandResult> runCommand() async {
|
||||||
await super.runCommand();
|
|
||||||
|
|
||||||
final String targetPlatform = argResults['target-platform'];
|
final String targetPlatform = argResults['target-platform'];
|
||||||
final TargetPlatform platform = getTargetPlatformForName(targetPlatform);
|
final TargetPlatform platform = getTargetPlatformForName(targetPlatform);
|
||||||
if (platform == null)
|
if (platform == null)
|
||||||
|
@ -20,12 +20,9 @@ class BuildFlxCommand extends BuildSubCommand {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FlutterCommandResult> runCommand() async {
|
Future<FlutterCommandResult> runCommand() async {
|
||||||
await super.runCommand();
|
|
||||||
|
|
||||||
printError("'build flx' is no longer supported. Instead, use 'build "
|
printError("'build flx' is no longer supported. Instead, use 'build "
|
||||||
"bundle' to build and assemble the application code and resources "
|
"bundle' to build and assemble the application code and resources "
|
||||||
'for your app.');
|
'for your app.');
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,6 @@ class BuildIOSCommand extends BuildSubCommand {
|
|||||||
final bool forSimulator = argResults['simulator'];
|
final bool forSimulator = argResults['simulator'];
|
||||||
defaultBuildMode = forSimulator ? BuildMode.debug : BuildMode.release;
|
defaultBuildMode = forSimulator ? BuildMode.debug : BuildMode.release;
|
||||||
|
|
||||||
await super.runCommand();
|
|
||||||
if (getCurrentHostPlatform() != HostPlatform.darwin_x64)
|
if (getCurrentHostPlatform() != HostPlatform.darwin_x64)
|
||||||
throwToolExit('Building for iOS is only supported on the Mac.');
|
throwToolExit('Building for iOS is only supported on the Mac.');
|
||||||
|
|
||||||
|
@ -384,15 +384,21 @@ class AppDomain extends Domain {
|
|||||||
|
|
||||||
return launch(
|
return launch(
|
||||||
runner,
|
runner,
|
||||||
({ Completer<DebugConnectionInfo> connectionInfoCompleter,
|
({
|
||||||
Completer<void> appStartedCompleter }) => runner.run(
|
Completer<DebugConnectionInfo> connectionInfoCompleter,
|
||||||
|
Completer<void> appStartedCompleter,
|
||||||
|
}) {
|
||||||
|
return runner.run(
|
||||||
connectionInfoCompleter: connectionInfoCompleter,
|
connectionInfoCompleter: connectionInfoCompleter,
|
||||||
appStartedCompleter: appStartedCompleter,
|
appStartedCompleter: appStartedCompleter,
|
||||||
route: route),
|
route: route,
|
||||||
|
);
|
||||||
|
},
|
||||||
device,
|
device,
|
||||||
projectDirectory,
|
projectDirectory,
|
||||||
enableHotReload,
|
enableHotReload,
|
||||||
cwd);
|
cwd,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AppInstance> launch(
|
Future<AppInstance> launch(
|
||||||
@ -428,9 +434,10 @@ class AppDomain extends Domain {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
final Completer<void> appStartedCompleter = Completer<void>();
|
final Completer<void> appStartedCompleter = Completer<void>();
|
||||||
// We don't want to wait for this future to complete and callbacks won't fail.
|
// We don't want to wait for this future to complete, and callbacks won't fail,
|
||||||
// As it just writes to stdout.
|
// as it just writes to stdout.
|
||||||
appStartedCompleter.future.then<void>((_) { // ignore: unawaited_futures
|
appStartedCompleter.future // ignore: unawaited_futures
|
||||||
|
.then<void>((void value) {
|
||||||
_sendAppEvent(app, 'started');
|
_sendAppEvent(app, 'started');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -438,7 +445,8 @@ class AppDomain extends Domain {
|
|||||||
try {
|
try {
|
||||||
await runOrAttach(
|
await runOrAttach(
|
||||||
connectionInfoCompleter: connectionInfoCompleter,
|
connectionInfoCompleter: connectionInfoCompleter,
|
||||||
appStartedCompleter: appStartedCompleter);
|
appStartedCompleter: appStartedCompleter,
|
||||||
|
);
|
||||||
_sendAppEvent(app, 'stop');
|
_sendAppEvent(app, 'stop');
|
||||||
} catch (error, trace) {
|
} catch (error, trace) {
|
||||||
_sendAppEvent(app, 'stop', <String, dynamic>{
|
_sendAppEvent(app, 'stop', <String, dynamic>{
|
||||||
@ -515,14 +523,15 @@ class AppDomain extends Domain {
|
|||||||
if (app == null)
|
if (app == null)
|
||||||
throw "app '$appId' not found";
|
throw "app '$appId' not found";
|
||||||
|
|
||||||
return app.stop().timeout(const Duration(seconds: 5)).then<bool>((_) {
|
return app.stop().then<bool>(
|
||||||
return true;
|
(void value) => true,
|
||||||
}).catchError((dynamic error) {
|
onError: (dynamic error, StackTrace stack) {
|
||||||
_sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
|
_sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
|
||||||
app.closeLogger();
|
app.closeLogger();
|
||||||
_apps.remove(app);
|
_apps.remove(app);
|
||||||
return false;
|
return false;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> detach(Map<String, dynamic> args) async {
|
Future<bool> detach(Map<String, dynamic> args) async {
|
||||||
@ -532,14 +541,15 @@ class AppDomain extends Domain {
|
|||||||
if (app == null)
|
if (app == null)
|
||||||
throw "app '$appId' not found";
|
throw "app '$appId' not found";
|
||||||
|
|
||||||
return app.detach().timeout(const Duration(seconds: 5)).then<bool>((_) {
|
return app.detach().then<bool>(
|
||||||
return true;
|
(void value) => true,
|
||||||
}).catchError((dynamic error) {
|
onError: (dynamic error, StackTrace stack) {
|
||||||
_sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
|
_sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
|
||||||
app.closeLogger();
|
app.closeLogger();
|
||||||
_apps.remove(app);
|
_apps.remove(app);
|
||||||
return false;
|
return false;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
AppInstance _getApp(String id) {
|
AppInstance _getApp(String id) {
|
||||||
@ -772,13 +782,14 @@ class NotifyingLogger extends Logger {
|
|||||||
@override
|
@override
|
||||||
Status startProgress(
|
Status startProgress(
|
||||||
String message, {
|
String message, {
|
||||||
|
@required Duration timeout,
|
||||||
String progressId,
|
String progressId,
|
||||||
bool expectSlowOperation = false,
|
|
||||||
bool multilineOutput,
|
bool multilineOutput,
|
||||||
int progressIndicatorPadding = kDefaultStatusPadding,
|
int progressIndicatorPadding = kDefaultStatusPadding,
|
||||||
}) {
|
}) {
|
||||||
|
assert(timeout != null);
|
||||||
printStatus(message);
|
printStatus(message);
|
||||||
return Status();
|
return SilentStatus(timeout: timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -948,11 +959,12 @@ class _AppRunLogger extends Logger {
|
|||||||
@override
|
@override
|
||||||
Status startProgress(
|
Status startProgress(
|
||||||
String message, {
|
String message, {
|
||||||
|
@required Duration timeout,
|
||||||
String progressId,
|
String progressId,
|
||||||
bool expectSlowOperation = false,
|
|
||||||
bool multilineOutput,
|
bool multilineOutput,
|
||||||
int progressIndicatorPadding = 52,
|
int progressIndicatorPadding = 52,
|
||||||
}) {
|
}) {
|
||||||
|
assert(timeout != null);
|
||||||
final int id = _nextProgressId++;
|
final int id = _nextProgressId++;
|
||||||
|
|
||||||
_sendProgressEvent(<String, dynamic>{
|
_sendProgressEvent(<String, dynamic>{
|
||||||
@ -961,13 +973,16 @@ class _AppRunLogger extends Logger {
|
|||||||
'message': message,
|
'message': message,
|
||||||
});
|
});
|
||||||
|
|
||||||
_status = Status(onFinish: () {
|
_status = SilentStatus(
|
||||||
|
timeout: timeout,
|
||||||
|
onFinish: () {
|
||||||
_status = null;
|
_status = null;
|
||||||
_sendProgressEvent(<String, dynamic>{
|
_sendProgressEvent(<String, dynamic>{
|
||||||
'id': id.toString(),
|
'id': id.toString(),
|
||||||
'progressId': progressId,
|
'progressId': progressId,
|
||||||
'finished': true
|
'finished': true,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
})..start();
|
})..start();
|
||||||
return _status;
|
return _status;
|
||||||
}
|
}
|
||||||
|
@ -29,11 +29,11 @@ class LogsCommand extends FlutterCommand {
|
|||||||
Device device;
|
Device device;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FlutterCommandResult> verifyThenRunCommand() async {
|
Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
|
||||||
device = await findTargetDevice();
|
device = await findTargetDevice();
|
||||||
if (device == null)
|
if (device == null)
|
||||||
throwToolExit(null);
|
throwToolExit(null);
|
||||||
return super.verifyThenRunCommand();
|
return super.verifyThenRunCommand(commandPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -17,9 +17,9 @@ import '../resident_runner.dart';
|
|||||||
import '../run_cold.dart';
|
import '../run_cold.dart';
|
||||||
import '../run_hot.dart';
|
import '../run_hot.dart';
|
||||||
import '../runner/flutter_command.dart';
|
import '../runner/flutter_command.dart';
|
||||||
|
import '../tracing.dart';
|
||||||
import 'daemon.dart';
|
import 'daemon.dart';
|
||||||
|
|
||||||
// TODO(mklim): Test this, flutter/flutter#23031.
|
|
||||||
abstract class RunCommandBase extends FlutterCommand {
|
abstract class RunCommandBase extends FlutterCommand {
|
||||||
// Used by run and drive commands.
|
// Used by run and drive commands.
|
||||||
RunCommandBase({ bool verboseHelp = false }) {
|
RunCommandBase({ bool verboseHelp = false }) {
|
||||||
@ -30,7 +30,7 @@ abstract class RunCommandBase extends FlutterCommand {
|
|||||||
argParser
|
argParser
|
||||||
..addFlag('trace-startup',
|
..addFlag('trace-startup',
|
||||||
negatable: false,
|
negatable: false,
|
||||||
help: 'Start tracing during startup.',
|
help: 'Trace application startup, then exit, saving the trace to a file.',
|
||||||
)
|
)
|
||||||
..addOption('route',
|
..addOption('route',
|
||||||
help: 'Which route to load when running the app.',
|
help: 'Which route to load when running the app.',
|
||||||
@ -90,6 +90,14 @@ class RunCommand extends RunCommandBase {
|
|||||||
help: 'Enable tracing of Skia code. This is useful when debugging '
|
help: 'Enable tracing of Skia code. This is useful when debugging '
|
||||||
'the GPU thread. By default, Flutter will not log skia code.',
|
'the GPU thread. By default, Flutter will not log skia code.',
|
||||||
)
|
)
|
||||||
|
..addFlag('await-first-frame-when-tracing',
|
||||||
|
defaultsTo: true,
|
||||||
|
help: 'Whether to wait for the first frame when tracing startup ("--trace-startup"), '
|
||||||
|
'or just dump the trace as soon as the application is running. The first frame '
|
||||||
|
'is detected by looking for a Timeline event with the name '
|
||||||
|
'"${Tracing.firstUsefulFrameEventName}". '
|
||||||
|
'By default, the widgets library\'s binding takes care of sending this event. ',
|
||||||
|
)
|
||||||
..addFlag('use-test-fonts',
|
..addFlag('use-test-fonts',
|
||||||
negatable: true,
|
negatable: true,
|
||||||
help: 'Enable (and default to) the "Ahem" font. This is a special font '
|
help: 'Enable (and default to) the "Ahem" font. This is a special font '
|
||||||
@ -108,7 +116,7 @@ class RunCommand extends RunCommandBase {
|
|||||||
)
|
)
|
||||||
..addFlag('track-widget-creation',
|
..addFlag('track-widget-creation',
|
||||||
hide: !verboseHelp,
|
hide: !verboseHelp,
|
||||||
help: 'Track widget creation locations. Requires Dart 2.0 functionality.',
|
help: 'Track widget creation locations.',
|
||||||
)
|
)
|
||||||
..addOption('project-root',
|
..addOption('project-root',
|
||||||
hide: !verboseHelp,
|
hide: !verboseHelp,
|
||||||
@ -123,18 +131,18 @@ class RunCommand extends RunCommandBase {
|
|||||||
..addFlag('hot',
|
..addFlag('hot',
|
||||||
negatable: true,
|
negatable: true,
|
||||||
defaultsTo: kHotReloadDefault,
|
defaultsTo: kHotReloadDefault,
|
||||||
help: 'Run with support for hot reloading.',
|
help: 'Run with support for hot reloading. Only available for debug mode. Not available with "--trace-startup".',
|
||||||
)
|
|
||||||
..addOption('pid-file',
|
|
||||||
help: 'Specify a file to write the process id to. '
|
|
||||||
'You can send SIGUSR1 to trigger a hot reload '
|
|
||||||
'and SIGUSR2 to trigger a hot restart.',
|
|
||||||
)
|
)
|
||||||
..addFlag('resident',
|
..addFlag('resident',
|
||||||
negatable: true,
|
negatable: true,
|
||||||
defaultsTo: true,
|
defaultsTo: true,
|
||||||
hide: !verboseHelp,
|
hide: !verboseHelp,
|
||||||
help: 'Stay resident after launching the application.',
|
help: 'Stay resident after launching the application. Not available with "--trace-startup".',
|
||||||
|
)
|
||||||
|
..addOption('pid-file',
|
||||||
|
help: 'Specify a file to write the process id to. '
|
||||||
|
'You can send SIGUSR1 to trigger a hot reload '
|
||||||
|
'and SIGUSR2 to trigger a hot restart.',
|
||||||
)
|
)
|
||||||
..addFlag('benchmark',
|
..addFlag('benchmark',
|
||||||
negatable: false,
|
negatable: false,
|
||||||
@ -206,7 +214,7 @@ class RunCommand extends RunCommandBase {
|
|||||||
|
|
||||||
bool shouldUseHotMode() {
|
bool shouldUseHotMode() {
|
||||||
final bool hotArg = argResults['hot'] ?? false;
|
final bool hotArg = argResults['hot'] ?? false;
|
||||||
final bool shouldUseHotMode = hotArg;
|
final bool shouldUseHotMode = hotArg && !traceStartup;
|
||||||
return getBuildInfo().isDebug && shouldUseHotMode;
|
return getBuildInfo().isDebug && shouldUseHotMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,6 +222,7 @@ class RunCommand extends RunCommandBase {
|
|||||||
argResults['use-application-binary'] != null;
|
argResults['use-application-binary'] != null;
|
||||||
|
|
||||||
bool get stayResident => argResults['resident'];
|
bool get stayResident => argResults['resident'];
|
||||||
|
bool get awaitFirstFrameWhenTracing => argResults['await-first-frame-when-tracing'];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> validateCommand() async {
|
Future<void> validateCommand() async {
|
||||||
@ -359,6 +368,7 @@ class RunCommand extends RunCommandBase {
|
|||||||
target: targetFile,
|
target: targetFile,
|
||||||
debuggingOptions: _createDebuggingOptions(),
|
debuggingOptions: _createDebuggingOptions(),
|
||||||
traceStartup: traceStartup,
|
traceStartup: traceStartup,
|
||||||
|
awaitFirstFrameWhenTracing: awaitFirstFrameWhenTracing,
|
||||||
applicationBinary: applicationBinaryPath == null
|
applicationBinary: applicationBinaryPath == null
|
||||||
? null
|
? null
|
||||||
: fs.file(applicationBinaryPath),
|
: fs.file(applicationBinaryPath),
|
||||||
|
@ -64,7 +64,7 @@ class ScreenshotCommand extends FlutterCommand {
|
|||||||
Device device;
|
Device device;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FlutterCommandResult> verifyThenRunCommand() async {
|
Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
|
||||||
device = await findTargetDevice();
|
device = await findTargetDevice();
|
||||||
if (device == null)
|
if (device == null)
|
||||||
throwToolExit('Must have a connected device');
|
throwToolExit('Must have a connected device');
|
||||||
@ -72,7 +72,7 @@ class ScreenshotCommand extends FlutterCommand {
|
|||||||
throwToolExit('Screenshot not supported for ${device.name}.');
|
throwToolExit('Screenshot not supported for ${device.name}.');
|
||||||
if (argResults[_kType] != _kDeviceType && argResults[_kObservatoryPort] == null)
|
if (argResults[_kType] != _kDeviceType && argResults[_kObservatoryPort] == null)
|
||||||
throwToolExit('Observatory port must be specified for screenshot type ${argResults[_kType]}');
|
throwToolExit('Observatory port must be specified for screenshot type ${argResults[_kType]}');
|
||||||
return super.verifyThenRunCommand();
|
return super.verifyThenRunCommand(commandPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -87,7 +87,7 @@ class UpdatePackagesCommand extends FlutterCommand {
|
|||||||
Future<void> _downloadCoverageData() async {
|
Future<void> _downloadCoverageData() async {
|
||||||
final Status status = logger.startProgress(
|
final Status status = logger.startProgress(
|
||||||
'Downloading lcov data for package:flutter...',
|
'Downloading lcov data for package:flutter...',
|
||||||
expectSlowOperation: true,
|
timeout: kSlowOperation,
|
||||||
);
|
);
|
||||||
final String urlBase = platform.environment['FLUTTER_STORAGE_BASE_URL'] ?? 'https://storage.googleapis.com';
|
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'));
|
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 String command = upgrade ? 'upgrade' : 'get';
|
||||||
final Status status = logger.startProgress(
|
final Status status = logger.startProgress(
|
||||||
'Running "flutter packages $command" in ${fs.path.basename(directory)}...',
|
'Running "flutter packages $command" in ${fs.path.basename(directory)}...',
|
||||||
expectSlowOperation: true,
|
timeout: kSlowOperation,
|
||||||
);
|
);
|
||||||
final List<String> args = <String>['--verbosity=warning'];
|
final List<String> args = <String>['--verbosity=warning'];
|
||||||
if (FlutterCommand.current != null && FlutterCommand.current.globalResults['verbose'])
|
if (FlutterCommand.current != null && FlutterCommand.current.globalResults['verbose'])
|
||||||
|
@ -156,7 +156,7 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery {
|
|||||||
final List<Device> devices = await pollingGetDevices().timeout(_pollingTimeout);
|
final List<Device> devices = await pollingGetDevices().timeout(_pollingTimeout);
|
||||||
_items.updateWithNewList(devices);
|
_items.updateWithNewList(devices);
|
||||||
} on TimeoutException {
|
} on TimeoutException {
|
||||||
printTrace('Device poll timed out.');
|
printTrace('Device poll timed out. Will retry.');
|
||||||
}
|
}
|
||||||
}, _pollingInterval);
|
}, _pollingInterval);
|
||||||
}
|
}
|
||||||
|
@ -188,7 +188,10 @@ class Doctor {
|
|||||||
|
|
||||||
for (ValidatorTask validatorTask in startValidatorTasks()) {
|
for (ValidatorTask validatorTask in startValidatorTasks()) {
|
||||||
final DoctorValidator validator = validatorTask.validator;
|
final DoctorValidator validator = validatorTask.validator;
|
||||||
final Status status = Status.withSpinner();
|
final Status status = Status.withSpinner(
|
||||||
|
timeout: kFastOperation,
|
||||||
|
slowWarningCallback: () => validator.slowWarning,
|
||||||
|
);
|
||||||
ValidationResult result;
|
ValidationResult result;
|
||||||
try {
|
try {
|
||||||
result = await validatorTask.result;
|
result = await validatorTask.result;
|
||||||
@ -290,6 +293,8 @@ abstract class DoctorValidator {
|
|||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
|
String get slowWarning => 'This is taking an unexpectedly long time...';
|
||||||
|
|
||||||
Future<ValidationResult> validate();
|
Future<ValidationResult> validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,6 +307,10 @@ class GroupedValidator extends DoctorValidator {
|
|||||||
|
|
||||||
final List<DoctorValidator> subValidators;
|
final List<DoctorValidator> subValidators;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get slowWarning => _currentSlowWarning;
|
||||||
|
String _currentSlowWarning = 'Initializing...';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ValidationResult> validate() async {
|
Future<ValidationResult> validate() async {
|
||||||
final List<ValidatorTask> tasks = <ValidatorTask>[];
|
final List<ValidatorTask> tasks = <ValidatorTask>[];
|
||||||
@ -311,8 +320,10 @@ class GroupedValidator extends DoctorValidator {
|
|||||||
|
|
||||||
final List<ValidationResult> results = <ValidationResult>[];
|
final List<ValidationResult> results = <ValidationResult>[];
|
||||||
for (ValidatorTask subValidator in tasks) {
|
for (ValidatorTask subValidator in tasks) {
|
||||||
|
_currentSlowWarning = subValidator.validator.slowWarning;
|
||||||
results.add(await subValidator.result);
|
results.add(await subValidator.result);
|
||||||
}
|
}
|
||||||
|
_currentSlowWarning = 'Merging results...';
|
||||||
return _mergeValidationResults(results);
|
return _mergeValidationResults(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -675,6 +686,9 @@ class IntelliJValidatorOnMac extends IntelliJValidator {
|
|||||||
class DeviceValidator extends DoctorValidator {
|
class DeviceValidator extends DoctorValidator {
|
||||||
DeviceValidator() : super('Connected device');
|
DeviceValidator() : super('Connected device');
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get slowWarning => 'Scanning for devices is taking a long time...';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ValidationResult> validate() async {
|
Future<ValidationResult> validate() async {
|
||||||
final List<Device> devices = await deviceManager.getAllConnectedDevices().toList();
|
final List<Device> devices = await deviceManager.getAllConnectedDevices().toList();
|
||||||
|
@ -325,7 +325,7 @@ class FuchsiaIsolateDiscoveryProtocol {
|
|||||||
}
|
}
|
||||||
_status ??= logger.startProgress(
|
_status ??= logger.startProgress(
|
||||||
'Waiting for a connection from $_isolateName on ${_device.name}...',
|
'Waiting for a connection from $_isolateName on ${_device.name}...',
|
||||||
expectSlowOperation: true,
|
timeout: null, // could take an arbitrary amount of time
|
||||||
);
|
);
|
||||||
_pollingTimer ??= Timer(_pollDuration, _findIsolate);
|
_pollingTimer ??= Timer(_pollDuration, _findIsolate);
|
||||||
return _foundUri.future.then((Uri uri) {
|
return _foundUri.future.then((Uri uri) {
|
||||||
|
@ -237,7 +237,7 @@ class CocoaPods {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _runPodInstall(IosProject iosProject, String engineDirectory) async {
|
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(
|
final ProcessResult result = await processManager.run(
|
||||||
<String>['pod', 'install', '--verbose'],
|
<String>['pod', 'install', '--verbose'],
|
||||||
workingDirectory: iosProject.hostAppRoot.path,
|
workingDirectory: iosProject.hostAppRoot.path,
|
||||||
|
@ -26,8 +26,6 @@ const String _kIdeviceinstallerInstructions =
|
|||||||
'To work with iOS devices, please install ideviceinstaller. To install, run:\n'
|
'To work with iOS devices, please install ideviceinstaller. To install, run:\n'
|
||||||
'brew install ideviceinstaller.';
|
'brew install ideviceinstaller.';
|
||||||
|
|
||||||
const Duration kPortForwardTimeout = Duration(seconds: 10);
|
|
||||||
|
|
||||||
class IOSDeploy {
|
class IOSDeploy {
|
||||||
const IOSDeploy();
|
const IOSDeploy();
|
||||||
|
|
||||||
@ -297,7 +295,7 @@ class IOSDevice extends Device {
|
|||||||
int installationResult = -1;
|
int installationResult = -1;
|
||||||
Uri localObservatoryUri;
|
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 (!debuggingOptions.debuggingEnabled) {
|
||||||
// If debugging is not enabled, just launch the application and continue.
|
// If debugging is not enabled, just launch the application and continue.
|
||||||
|
@ -470,7 +470,7 @@ Future<XcodeBuildResult> buildXcodeProject({
|
|||||||
initialBuildStatus.cancel();
|
initialBuildStatus.cancel();
|
||||||
buildSubStatus = logger.startProgress(
|
buildSubStatus = logger.startProgress(
|
||||||
line,
|
line,
|
||||||
expectSlowOperation: true,
|
timeout: kSlowOperation,
|
||||||
progressIndicatorPadding: kDefaultStatusPadding - 7,
|
progressIndicatorPadding: kDefaultStatusPadding - 7,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -485,7 +485,7 @@ Future<XcodeBuildResult> buildXcodeProject({
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Stopwatch buildStopwatch = Stopwatch()..start();
|
final Stopwatch buildStopwatch = Stopwatch()..start();
|
||||||
initialBuildStatus = logger.startProgress('Starting Xcode build...');
|
initialBuildStatus = logger.startProgress('Starting Xcode build...', timeout: kFastOperation);
|
||||||
final RunResult buildResult = await runAsync(
|
final RunResult buildResult = await runAsync(
|
||||||
buildCommands,
|
buildCommands,
|
||||||
workingDirectory: app.project.hostAppRoot.path,
|
workingDirectory: app.project.hostAppRoot.path,
|
||||||
|
@ -9,7 +9,6 @@ import 'package:meta/meta.dart';
|
|||||||
import 'application_package.dart';
|
import 'application_package.dart';
|
||||||
import 'artifacts.dart';
|
import 'artifacts.dart';
|
||||||
import 'asset.dart';
|
import 'asset.dart';
|
||||||
import 'base/common.dart';
|
|
||||||
import 'base/file_system.dart';
|
import 'base/file_system.dart';
|
||||||
import 'base/io.dart';
|
import 'base/io.dart';
|
||||||
import 'base/logger.dart';
|
import 'base/logger.dart';
|
||||||
@ -76,12 +75,14 @@ class FlutterDevice {
|
|||||||
if (vmServices != null)
|
if (vmServices != null)
|
||||||
return;
|
return;
|
||||||
final List<VMService> localVmServices = List<VMService>(observatoryUris.length);
|
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]}');
|
printTrace('Connecting to service protocol: ${observatoryUris[i]}');
|
||||||
localVmServices[i] = await VMService.connect(observatoryUris[i],
|
localVmServices[i] = await VMService.connect(
|
||||||
|
observatoryUris[i],
|
||||||
reloadSources: reloadSources,
|
reloadSources: reloadSources,
|
||||||
restart: restart,
|
restart: restart,
|
||||||
compileExpression: compileExpression);
|
compileExpression: compileExpression,
|
||||||
|
);
|
||||||
printTrace('Successfully connected to service protocol: ${observatoryUris[i]}');
|
printTrace('Successfully connected to service protocol: ${observatoryUris[i]}');
|
||||||
}
|
}
|
||||||
vmServices = localVmServices;
|
vmServices = localVmServices;
|
||||||
@ -102,9 +103,13 @@ class FlutterDevice {
|
|||||||
|
|
||||||
return vmServices
|
return vmServices
|
||||||
.where((VMService service) => !service.isClosed)
|
.where((VMService service) => !service.isClosed)
|
||||||
.expand<FlutterView>((VMService service) => viewFilter != null
|
.expand<FlutterView>(
|
||||||
|
(VMService service) {
|
||||||
|
return viewFilter != null
|
||||||
? service.vm.allViewsWithName(viewFilter)
|
? service.vm.allViewsWithName(viewFilter)
|
||||||
: service.vm.views)
|
: service.vm.views;
|
||||||
|
},
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,13 +125,16 @@ class FlutterDevice {
|
|||||||
final List<FlutterView> flutterViews = views;
|
final List<FlutterView> flutterViews = views;
|
||||||
if (flutterViews == null || flutterViews.isEmpty)
|
if (flutterViews == null || flutterViews.isEmpty)
|
||||||
return;
|
return;
|
||||||
|
final List<Future<void>> futures = <Future<void>>[];
|
||||||
for (FlutterView view in flutterViews) {
|
for (FlutterView view in flutterViews) {
|
||||||
if (view != null && view.uiIsolate != null) {
|
if (view != null && view.uiIsolate != null) {
|
||||||
// Manage waits specifically below.
|
futures.add(view.uiIsolate.flutterExit());
|
||||||
view.uiIsolate.flutterExit(); // ignore: unawaited_futures
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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,
|
Future<Uri> setupDevFS(String fsName,
|
||||||
@ -390,7 +398,7 @@ class FlutterDevice {
|
|||||||
}) async {
|
}) async {
|
||||||
final Status devFSStatus = logger.startProgress(
|
final Status devFSStatus = logger.startProgress(
|
||||||
'Syncing files to device ${device.name}...',
|
'Syncing files to device ${device.name}...',
|
||||||
expectSlowOperation: true,
|
timeout: kFastOperation,
|
||||||
);
|
);
|
||||||
UpdateFSReport report;
|
UpdateFSReport report;
|
||||||
try {
|
try {
|
||||||
@ -482,11 +490,14 @@ abstract class ResidentRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Start the app and keep the process running during its lifetime.
|
/// 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({
|
Future<int> run({
|
||||||
Completer<DebugConnectionInfo> connectionInfoCompleter,
|
Completer<DebugConnectionInfo> connectionInfoCompleter,
|
||||||
Completer<void> appStartedCompleter,
|
Completer<void> appStartedCompleter,
|
||||||
String route,
|
String route,
|
||||||
bool shouldBuild = true
|
bool shouldBuild = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<int> attach({
|
Future<int> attach({
|
||||||
@ -506,7 +517,7 @@ abstract class ResidentRunner {
|
|||||||
await _debugSaveCompilationTrace();
|
await _debugSaveCompilationTrace();
|
||||||
await stopEchoingDeviceLog();
|
await stopEchoingDeviceLog();
|
||||||
await preStop();
|
await preStop();
|
||||||
return stopApp();
|
await stopApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> detach() async {
|
Future<void> detach() async {
|
||||||
@ -571,7 +582,7 @@ abstract class ResidentRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _screenshot(FlutterDevice device) async {
|
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');
|
final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png');
|
||||||
try {
|
try {
|
||||||
if (supportsServiceProtocol && isRunningDebug) {
|
if (supportsServiceProtocol && isRunningDebug) {
|
||||||
@ -686,14 +697,18 @@ abstract class ResidentRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// If the [reloadSources] parameter is not null the 'reloadSources' service
|
/// 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({
|
Future<void> connectToServiceProtocol({
|
||||||
ReloadSources reloadSources,
|
ReloadSources reloadSources,
|
||||||
Restart restart,
|
Restart restart,
|
||||||
CompileExpression compileExpression,
|
CompileExpression compileExpression,
|
||||||
}) async {
|
}) async {
|
||||||
if (!debuggingOptions.debuggingEnabled)
|
if (!debuggingOptions.debuggingEnabled)
|
||||||
return Future<void>.error('Error the service protocol is not enabled.');
|
throw 'The service protocol is not enabled.';
|
||||||
|
|
||||||
bool viewFound = false;
|
bool viewFound = false;
|
||||||
for (FlutterDevice device in flutterDevices) {
|
for (FlutterDevice device in flutterDevices) {
|
||||||
@ -704,13 +719,15 @@ abstract class ResidentRunner {
|
|||||||
);
|
);
|
||||||
await device.getVMs();
|
await device.getVMs();
|
||||||
await device.refreshViews();
|
await device.refreshViews();
|
||||||
if (device.views.isEmpty)
|
if (device.views.isNotEmpty)
|
||||||
printStatus('No Flutter views available on ${device.device.name}');
|
|
||||||
else
|
|
||||||
viewFound = true;
|
viewFound = true;
|
||||||
}
|
}
|
||||||
if (!viewFound)
|
if (!viewFound) {
|
||||||
throwToolExit('No Flutter view is available');
|
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.
|
// Listen for service protocol connection to close.
|
||||||
for (FlutterDevice device in flutterDevices) {
|
for (FlutterDevice device in flutterDevices) {
|
||||||
@ -861,12 +878,13 @@ abstract class ResidentRunner {
|
|||||||
printHelp(details: false);
|
printHelp(details: false);
|
||||||
}
|
}
|
||||||
terminal.singleCharMode = true;
|
terminal.singleCharMode = true;
|
||||||
terminal.onCharInput.listen(processTerminalInput);
|
terminal.keystrokes.listen(processTerminalInput);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> waitForAppToFinish() async {
|
Future<int> waitForAppToFinish() async {
|
||||||
final int exitCode = await _finished.future;
|
final int exitCode = await _finished.future;
|
||||||
|
assert(exitCode != null);
|
||||||
await cleanupAtFinish();
|
await cleanupAtFinish();
|
||||||
return exitCode;
|
return exitCode;
|
||||||
}
|
}
|
||||||
@ -887,8 +905,10 @@ abstract class ResidentRunner {
|
|||||||
Future<void> preStop() async { }
|
Future<void> preStop() async { }
|
||||||
|
|
||||||
Future<void> stopApp() async {
|
Future<void> stopApp() async {
|
||||||
|
final List<Future<void>> futures = <Future<void>>[];
|
||||||
for (FlutterDevice device in flutterDevices)
|
for (FlutterDevice device in flutterDevices)
|
||||||
await device.stopApps();
|
futures.add(device.stopApps());
|
||||||
|
await Future.wait(futures);
|
||||||
appFinished();
|
appFinished();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ class ColdRunner extends ResidentRunner {
|
|||||||
DebuggingOptions debuggingOptions,
|
DebuggingOptions debuggingOptions,
|
||||||
bool usesTerminalUI = true,
|
bool usesTerminalUI = true,
|
||||||
this.traceStartup = false,
|
this.traceStartup = false,
|
||||||
|
this.awaitFirstFrameWhenTracing = true,
|
||||||
this.applicationBinary,
|
this.applicationBinary,
|
||||||
bool saveCompilationTrace = false,
|
bool saveCompilationTrace = false,
|
||||||
bool stayResident = true,
|
bool stayResident = true,
|
||||||
@ -34,6 +35,7 @@ class ColdRunner extends ResidentRunner {
|
|||||||
ipv6: ipv6);
|
ipv6: ipv6);
|
||||||
|
|
||||||
final bool traceStartup;
|
final bool traceStartup;
|
||||||
|
final bool awaitFirstFrameWhenTracing;
|
||||||
final File applicationBinary;
|
final File applicationBinary;
|
||||||
bool _didAttach = false;
|
bool _didAttach = false;
|
||||||
|
|
||||||
@ -66,8 +68,14 @@ class ColdRunner extends ResidentRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Connect to observatory.
|
// Connect to observatory.
|
||||||
if (debuggingOptions.debuggingEnabled)
|
if (debuggingOptions.debuggingEnabled) {
|
||||||
|
try {
|
||||||
await connectToServiceProtocol();
|
await connectToServiceProtocol();
|
||||||
|
} on String catch (message) {
|
||||||
|
printError(message);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (flutterDevices.first.observatoryUris != null) {
|
if (flutterDevices.first.observatoryUris != null) {
|
||||||
// For now, only support one debugger connection.
|
// For now, only support one debugger connection.
|
||||||
@ -91,13 +99,11 @@ class ColdRunner extends ResidentRunner {
|
|||||||
// Only trace startup for the first device.
|
// Only trace startup for the first device.
|
||||||
final FlutterDevice device = flutterDevices.first;
|
final FlutterDevice device = flutterDevices.first;
|
||||||
if (device.vmServices != null && device.vmServices.isNotEmpty) {
|
if (device.vmServices != null && device.vmServices.isNotEmpty) {
|
||||||
printStatus('Downloading startup trace info for ${device.device.name}');
|
printStatus('Tracing startup on ${device.device.name}.');
|
||||||
try {
|
await downloadStartupTrace(
|
||||||
await downloadStartupTrace(device.vmServices.first);
|
device.vmServices.first,
|
||||||
} catch (error) {
|
awaitFirstFrame: awaitFirstFrameWhenTracing,
|
||||||
printError('Error downloading startup trace: $error');
|
);
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
appFinished();
|
appFinished();
|
||||||
} else if (stayResident) {
|
} else if (stayResident) {
|
||||||
@ -107,7 +113,7 @@ class ColdRunner extends ResidentRunner {
|
|||||||
|
|
||||||
appStartedCompleter?.complete();
|
appStartedCompleter?.complete();
|
||||||
|
|
||||||
if (stayResident)
|
if (stayResident && !traceStartup)
|
||||||
return waitForAppToFinish();
|
return waitForAppToFinish();
|
||||||
await cleanupAtFinish();
|
await cleanupAtFinish();
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -176,6 +176,7 @@ class HotRunner extends ResidentRunner {
|
|||||||
throw 'Failed to compile $expression';
|
throw 'Failed to compile $expression';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the exit code of the flutter tool process, like [run].
|
||||||
@override
|
@override
|
||||||
Future<int> attach({
|
Future<int> attach({
|
||||||
Completer<DebugConnectionInfo> connectionInfoCompleter,
|
Completer<DebugConnectionInfo> connectionInfoCompleter,
|
||||||
@ -260,10 +261,11 @@ class HotRunner extends ResidentRunner {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int result = 0;
|
||||||
if (stayResident)
|
if (stayResident)
|
||||||
return waitForAppToFinish();
|
result = await waitForAppToFinish();
|
||||||
await cleanupAtFinish();
|
await cleanupAtFinish();
|
||||||
return 0;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -409,22 +411,21 @@ class HotRunner extends ResidentRunner {
|
|||||||
device.devFS.assetPathsToEvict.clear();
|
device.devFS.assetPathsToEvict.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _cleanupDevFS() {
|
Future<void> _cleanupDevFS() async {
|
||||||
final List<Future<void>> futures = <Future<void>>[];
|
final List<Future<void>> futures = <Future<void>>[];
|
||||||
for (FlutterDevice device in flutterDevices) {
|
for (FlutterDevice device in flutterDevices) {
|
||||||
if (device.devFS != null) {
|
if (device.devFS != null) {
|
||||||
// Cleanup the devFS; don't wait indefinitely, and ignore any errors.
|
// Cleanup the devFS, but don't wait indefinitely.
|
||||||
|
// We ignore any errors, because it's not clear what we would do anyway.
|
||||||
futures.add(device.devFS.destroy()
|
futures.add(device.devFS.destroy()
|
||||||
.timeout(const Duration(milliseconds: 250))
|
.timeout(const Duration(milliseconds: 250))
|
||||||
.catchError((dynamic error) {
|
.catchError((dynamic error) {
|
||||||
printTrace('$error');
|
printTrace('Ignored error while cleaning up DevFS: $error');
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
device.devFS = null;
|
device.devFS = null;
|
||||||
}
|
}
|
||||||
final Completer<void> completer = Completer<void>();
|
await Future.wait(futures);
|
||||||
Future.wait(futures).whenComplete(() { completer.complete(null); } ); // ignore: unawaited_futures
|
|
||||||
return completer.future;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _launchInView(FlutterDevice device,
|
Future<void> _launchInView(FlutterDevice device,
|
||||||
@ -575,6 +576,7 @@ class HotRunner extends ResidentRunner {
|
|||||||
}
|
}
|
||||||
final Status status = logger.startProgress(
|
final Status status = logger.startProgress(
|
||||||
'Performing hot restart...',
|
'Performing hot restart...',
|
||||||
|
timeout: kFastOperation,
|
||||||
progressId: 'hot.restart',
|
progressId: 'hot.restart',
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
@ -591,26 +593,45 @@ class HotRunner extends ResidentRunner {
|
|||||||
} else {
|
} else {
|
||||||
final bool reloadOnTopOfSnapshot = _runningFromSnapshot;
|
final bool reloadOnTopOfSnapshot = _runningFromSnapshot;
|
||||||
final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing';
|
final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing';
|
||||||
final Status status = logger.startProgress(
|
Status status = logger.startProgress(
|
||||||
'$progressPrefix hot reload...',
|
'$progressPrefix hot reload...',
|
||||||
progressId: 'hot.reload'
|
timeout: kFastOperation,
|
||||||
|
progressId: 'hot.reload',
|
||||||
);
|
);
|
||||||
OperationResult result;
|
OperationResult result;
|
||||||
|
bool showTime = true;
|
||||||
try {
|
try {
|
||||||
result = await _reloadSources(pause: pauseAfterRestart, reason: reason);
|
result = await _reloadSources(
|
||||||
|
pause: pauseAfterRestart,
|
||||||
|
reason: reason,
|
||||||
|
onSlow: (String message) {
|
||||||
|
status?.cancel();
|
||||||
|
status = logger.startProgress(
|
||||||
|
message,
|
||||||
|
timeout: kSlowOperation,
|
||||||
|
progressId: 'hot.reload',
|
||||||
|
);
|
||||||
|
showTime = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
status.cancel();
|
status.cancel();
|
||||||
}
|
}
|
||||||
if (result.isOk)
|
if (result.isOk) {
|
||||||
|
if (showTime) {
|
||||||
printStatus('${result.message} in ${getElapsedAsMilliseconds(timer.elapsed)}.');
|
printStatus('${result.message} in ${getElapsedAsMilliseconds(timer.elapsed)}.');
|
||||||
|
} else {
|
||||||
|
printStatus('${result.message}.');
|
||||||
|
}
|
||||||
|
}
|
||||||
if (result.hintMessage != null)
|
if (result.hintMessage != null)
|
||||||
printStatus('\n${result.hintMessage}');
|
printStatus('\n${result.hintMessage}');
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<OperationResult> _reloadSources({ bool pause = false, String reason }) async {
|
Future<OperationResult> _reloadSources({ bool pause = false, String reason, void Function(String message) onSlow }) async {
|
||||||
final Map<String, String> analyticsParameters = <String, String> {};
|
final Map<String, String> analyticsParameters = <String, String>{};
|
||||||
if (reason != null) {
|
if (reason != null) {
|
||||||
analyticsParameters[kEventReloadReasonParameterName] = reason;
|
analyticsParameters[kEventReloadReasonParameterName] = reason;
|
||||||
}
|
}
|
||||||
@ -621,25 +642,24 @@ class HotRunner extends ResidentRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_isPaused()) {
|
|
||||||
printTrace('Refreshing active FlutterViews before reloading.');
|
|
||||||
await refreshViews();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The initial launch is from a script snapshot. When we reload from source
|
// The initial launch is from a script snapshot. When we reload from source
|
||||||
// on top of a script snapshot, the first reload will be a worst case reload
|
// on top of a script snapshot, the first reload will be a worst case reload
|
||||||
// because all of the sources will end up being dirty (library paths will
|
// because all of the sources will end up being dirty (library paths will
|
||||||
// change from host path to a device path). Subsequent reloads will
|
// change from host path to a device path). Subsequent reloads will
|
||||||
// not be affected, so we resume reporting reload times on the second
|
// not be affected, so we resume reporting reload times on the second
|
||||||
// reload.
|
// reload.
|
||||||
final bool shouldReportReloadTime = !_runningFromSnapshot;
|
bool shouldReportReloadTime = !_runningFromSnapshot;
|
||||||
final Stopwatch reloadTimer = Stopwatch()..start();
|
final Stopwatch reloadTimer = Stopwatch()..start();
|
||||||
|
|
||||||
|
if (!_isPaused()) {
|
||||||
|
printTrace('Refreshing active FlutterViews before reloading.');
|
||||||
|
await refreshViews();
|
||||||
|
}
|
||||||
|
|
||||||
final Stopwatch devFSTimer = Stopwatch()..start();
|
final Stopwatch devFSTimer = Stopwatch()..start();
|
||||||
final UpdateFSReport updatedDevFS = await _updateDevFS();
|
final UpdateFSReport updatedDevFS = await _updateDevFS();
|
||||||
// Record time it took to synchronize to DevFS.
|
// Record time it took to synchronize to DevFS.
|
||||||
_addBenchmarkData('hotReloadDevFSSyncMilliseconds',
|
_addBenchmarkData('hotReloadDevFSSyncMilliseconds', devFSTimer.elapsed.inMilliseconds);
|
||||||
devFSTimer.elapsed.inMilliseconds);
|
|
||||||
if (!updatedDevFS.success)
|
if (!updatedDevFS.success)
|
||||||
return OperationResult(1, 'DevFS synchronization failed');
|
return OperationResult(1, 'DevFS synchronization failed');
|
||||||
String reloadMessage;
|
String reloadMessage;
|
||||||
@ -656,7 +676,6 @@ class HotRunner extends ResidentRunner {
|
|||||||
// running from snapshot to running from uploaded files.
|
// running from snapshot to running from uploaded files.
|
||||||
await device.resetAssetDirectory();
|
await device.resetAssetDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Completer<DeviceReloadReport> completer = Completer<DeviceReloadReport>();
|
final Completer<DeviceReloadReport> completer = Completer<DeviceReloadReport>();
|
||||||
allReportsFutures.add(completer.future);
|
allReportsFutures.add(completer.future);
|
||||||
final List<Future<Map<String, dynamic>>> reportFutures = device.reloadSources(
|
final List<Future<Map<String, dynamic>>> reportFutures = device.reloadSources(
|
||||||
@ -672,7 +691,6 @@ class HotRunner extends ResidentRunner {
|
|||||||
completer.complete(DeviceReloadReport(device, reports));
|
completer.complete(DeviceReloadReport(device, reports));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures);
|
final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures);
|
||||||
for (DeviceReloadReport report in reports) {
|
for (DeviceReloadReport report in reports) {
|
||||||
final Map<String, dynamic> reloadReport = report.reports[0];
|
final Map<String, dynamic> reloadReport = report.reports[0];
|
||||||
@ -700,29 +718,26 @@ class HotRunner extends ResidentRunner {
|
|||||||
reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
|
reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on Map<String, dynamic> catch (error, st) {
|
} on Map<String, dynamic> catch (error, stackTrace) {
|
||||||
printError('Hot reload failed: $error\n$st');
|
printTrace('Hot reload failed: $error\n$stackTrace');
|
||||||
final int errorCode = error['code'];
|
final int errorCode = error['code'];
|
||||||
final String errorMessage = error['message'];
|
String errorMessage = error['message'];
|
||||||
if (errorCode == Isolate.kIsolateReloadBarred) {
|
if (errorCode == Isolate.kIsolateReloadBarred) {
|
||||||
printError(
|
errorMessage = 'Unable to hot reload application due to an unrecoverable error in '
|
||||||
'Unable to hot reload application due to an unrecoverable error in '
|
|
||||||
'the source code. Please address the error and then use "R" to '
|
'the source code. Please address the error and then use "R" to '
|
||||||
'restart the app.'
|
'restart the app.\n'
|
||||||
);
|
'$errorMessage (error code: $errorCode)';
|
||||||
flutterUsage.sendEvent('hot', 'reload-barred');
|
flutterUsage.sendEvent('hot', 'reload-barred');
|
||||||
return OperationResult(errorCode, errorMessage);
|
return OperationResult(errorCode, errorMessage);
|
||||||
}
|
}
|
||||||
|
return OperationResult(errorCode, '$errorMessage (error code: $errorCode)');
|
||||||
printError('Hot reload failed:\ncode = $errorCode\nmessage = $errorMessage\n$st');
|
} catch (error, stackTrace) {
|
||||||
return OperationResult(errorCode, errorMessage);
|
printTrace('Hot reload failed: $error\n$stackTrace');
|
||||||
} catch (error, st) {
|
|
||||||
printError('Hot reload failed: $error\n$st');
|
|
||||||
return OperationResult(1, '$error');
|
return OperationResult(1, '$error');
|
||||||
}
|
}
|
||||||
// Record time it took for the VM to reload the sources.
|
// Record time it took for the VM to reload the sources.
|
||||||
_addBenchmarkData('hotReloadVMReloadMilliseconds',
|
_addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds);
|
||||||
vmReloadTimer.elapsed.inMilliseconds);
|
|
||||||
final Stopwatch reassembleTimer = Stopwatch()..start();
|
final Stopwatch reassembleTimer = Stopwatch()..start();
|
||||||
// Reload the isolate.
|
// Reload the isolate.
|
||||||
final List<Future<void>> allDevices = <Future<void>>[];
|
final List<Future<void>> allDevices = <Future<void>>[];
|
||||||
@ -739,53 +754,97 @@ class HotRunner extends ResidentRunner {
|
|||||||
});
|
});
|
||||||
allDevices.add(deviceCompleter.future);
|
allDevices.add(deviceCompleter.future);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Future.wait(allDevices);
|
await Future.wait(allDevices);
|
||||||
// We are now running from source.
|
// We are now running from source.
|
||||||
_runningFromSnapshot = false;
|
_runningFromSnapshot = false;
|
||||||
// Check if the isolate is paused.
|
// Check if any isolates are paused.
|
||||||
|
|
||||||
final List<FlutterView> reassembleViews = <FlutterView>[];
|
final List<FlutterView> reassembleViews = <FlutterView>[];
|
||||||
|
String serviceEventKind;
|
||||||
|
int pausedIsolatesFound = 0;
|
||||||
for (FlutterDevice device in flutterDevices) {
|
for (FlutterDevice device in flutterDevices) {
|
||||||
for (FlutterView view in device.views) {
|
for (FlutterView view in device.views) {
|
||||||
// Check if the isolate is paused, and if so, don't reassemble. Ignore the
|
// Check if the isolate is paused, and if so, don't reassemble. Ignore the
|
||||||
// PostPauseEvent event - the client requesting the pause will resume the app.
|
// PostPauseEvent event - the client requesting the pause will resume the app.
|
||||||
final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
|
final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
|
||||||
if (pauseEvent != null && pauseEvent.isPauseEvent && pauseEvent.kind != ServiceEvent.kPausePostRequest) {
|
if (pauseEvent != null && pauseEvent.isPauseEvent && pauseEvent.kind != ServiceEvent.kPausePostRequest) {
|
||||||
continue;
|
pausedIsolatesFound += 1;
|
||||||
|
if (serviceEventKind == null) {
|
||||||
|
serviceEventKind = pauseEvent.kind;
|
||||||
|
} else if (serviceEventKind != pauseEvent.kind) {
|
||||||
|
serviceEventKind = ''; // many kinds
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
reassembleViews.add(view);
|
reassembleViews.add(view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (pausedIsolatesFound > 0) {
|
||||||
|
if (onSlow != null)
|
||||||
|
onSlow('${_describePausedIsolates(pausedIsolatesFound, serviceEventKind)}; interface might not update.');
|
||||||
if (reassembleViews.isEmpty) {
|
if (reassembleViews.isEmpty) {
|
||||||
printTrace('Skipping reassemble because all isolates are paused.');
|
printTrace('Skipping reassemble because all isolates are paused.');
|
||||||
return OperationResult(OperationResult.ok.code, reloadMessage);
|
return OperationResult(OperationResult.ok.code, reloadMessage);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
assert(reassembleViews.isNotEmpty);
|
||||||
printTrace('Evicting dirty assets');
|
printTrace('Evicting dirty assets');
|
||||||
await _evictDirtyAssets();
|
await _evictDirtyAssets();
|
||||||
printTrace('Reassembling application');
|
printTrace('Reassembling application');
|
||||||
bool reassembleAndScheduleErrors = false;
|
bool failedReassemble = false;
|
||||||
bool reassembleTimedOut = false;
|
|
||||||
final List<Future<void>> futures = <Future<void>>[];
|
final List<Future<void>> futures = <Future<void>>[];
|
||||||
for (FlutterView view in reassembleViews) {
|
for (FlutterView view in reassembleViews) {
|
||||||
futures.add(view.uiIsolate.flutterReassemble().then((_) {
|
futures.add(() async {
|
||||||
return view.uiIsolate.uiWindowScheduleFrame();
|
try {
|
||||||
}).catchError((dynamic error) {
|
await view.uiIsolate.flutterReassemble();
|
||||||
if (error is TimeoutException) {
|
} catch (error) {
|
||||||
reassembleTimedOut = true;
|
failedReassemble = true;
|
||||||
printTrace('Reassembling ${view.uiIsolate.name} took too long.');
|
|
||||||
printStatus('Hot reloading ${view.uiIsolate.name} took too long; the reload may have failed.');
|
|
||||||
} else {
|
|
||||||
reassembleAndScheduleErrors = true;
|
|
||||||
printError('Reassembling ${view.uiIsolate.name} failed: $error');
|
printError('Reassembling ${view.uiIsolate.name} failed: $error');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}));
|
try {
|
||||||
|
await view.uiIsolate.uiWindowScheduleFrame();
|
||||||
|
} catch (error) {
|
||||||
|
failedReassemble = true;
|
||||||
|
printError('Scheduling a frame for ${view.uiIsolate.name} failed: $error');
|
||||||
}
|
}
|
||||||
await Future.wait(futures);
|
}());
|
||||||
|
}
|
||||||
|
final Future<void> reassembleFuture = Future.wait<void>(futures).then<void>((List<void> values) { });
|
||||||
|
await reassembleFuture.timeout(
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
onTimeout: () async {
|
||||||
|
if (pausedIsolatesFound > 0) {
|
||||||
|
shouldReportReloadTime = false;
|
||||||
|
return; // probably no point waiting, they're probably deadlocked and we've already warned.
|
||||||
|
}
|
||||||
|
// Check if any isolate is newly paused.
|
||||||
|
printTrace('This is taking a long time; will now check for paused isolates.');
|
||||||
|
int postReloadPausedIsolatesFound = 0;
|
||||||
|
String serviceEventKind;
|
||||||
|
for (FlutterView view in reassembleViews) {
|
||||||
|
await view.uiIsolate.reload();
|
||||||
|
final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
|
||||||
|
if (pauseEvent != null && pauseEvent.isPauseEvent) {
|
||||||
|
postReloadPausedIsolatesFound += 1;
|
||||||
|
if (serviceEventKind == null) {
|
||||||
|
serviceEventKind = pauseEvent.kind;
|
||||||
|
} else if (serviceEventKind != pauseEvent.kind) {
|
||||||
|
serviceEventKind = ''; // many kinds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printTrace('Found $postReloadPausedIsolatesFound newly paused isolate(s).');
|
||||||
|
if (postReloadPausedIsolatesFound == 0) {
|
||||||
|
await reassembleFuture; // must just be taking a long time... keep waiting!
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shouldReportReloadTime = false;
|
||||||
|
if (onSlow != null)
|
||||||
|
onSlow('${_describePausedIsolates(postReloadPausedIsolatesFound, serviceEventKind)}.');
|
||||||
|
},
|
||||||
|
);
|
||||||
// Record time it took for Flutter to reassemble the application.
|
// Record time it took for Flutter to reassemble the application.
|
||||||
_addBenchmarkData('hotReloadFlutterReassembleMilliseconds',
|
_addBenchmarkData('hotReloadFlutterReassembleMilliseconds', reassembleTimer.elapsed.inMilliseconds);
|
||||||
reassembleTimer.elapsed.inMilliseconds);
|
|
||||||
|
|
||||||
reloadTimer.stop();
|
reloadTimer.stop();
|
||||||
final Duration reloadDuration = reloadTimer.elapsed;
|
final Duration reloadDuration = reloadTimer.elapsed;
|
||||||
@ -794,23 +853,51 @@ class HotRunner extends ResidentRunner {
|
|||||||
analyticsParameters[kEventReloadOverallTimeInMs] = '$reloadInMs';
|
analyticsParameters[kEventReloadOverallTimeInMs] = '$reloadInMs';
|
||||||
flutterUsage.sendEvent('hot', 'reload', parameters: analyticsParameters);
|
flutterUsage.sendEvent('hot', 'reload', parameters: analyticsParameters);
|
||||||
|
|
||||||
printTrace('Hot reload performed in $reloadInMs.');
|
if (shouldReportReloadTime) {
|
||||||
|
printTrace('Hot reload performed in ${getElapsedAsMilliseconds(reloadDuration)}.');
|
||||||
// Record complete time it took for the reload.
|
// Record complete time it took for the reload.
|
||||||
_addBenchmarkData('hotReloadMillisecondsToFrame', reloadInMs);
|
_addBenchmarkData('hotReloadMillisecondsToFrame', reloadInMs);
|
||||||
// Only report timings if we reloaded a single view without any
|
}
|
||||||
// errors or timeouts.
|
// Only report timings if we reloaded a single view without any errors.
|
||||||
if ((reassembleViews.length == 1) &&
|
if ((reassembleViews.length == 1) && !failedReassemble && shouldReportReloadTime)
|
||||||
!reassembleAndScheduleErrors &&
|
|
||||||
!reassembleTimedOut &&
|
|
||||||
shouldReportReloadTime)
|
|
||||||
flutterUsage.sendTiming('hot', 'reload', reloadDuration);
|
flutterUsage.sendTiming('hot', 'reload', reloadDuration);
|
||||||
|
|
||||||
return OperationResult(
|
return OperationResult(
|
||||||
reassembleAndScheduleErrors ? 1 : OperationResult.ok.code,
|
failedReassemble ? 1 : OperationResult.ok.code,
|
||||||
reloadMessage,
|
reloadMessage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _describePausedIsolates(int pausedIsolatesFound, String serviceEventKind) {
|
||||||
|
assert(pausedIsolatesFound > 0);
|
||||||
|
final StringBuffer message = StringBuffer();
|
||||||
|
bool plural;
|
||||||
|
if (pausedIsolatesFound == 1) {
|
||||||
|
if (flutterDevices.length == 1 && flutterDevices.single.views.length == 1) {
|
||||||
|
message.write('The application is ');
|
||||||
|
} else {
|
||||||
|
message.write('An isolate is ');
|
||||||
|
}
|
||||||
|
plural = false;
|
||||||
|
} else {
|
||||||
|
message.write('$pausedIsolatesFound isolates are ');
|
||||||
|
plural = true;
|
||||||
|
}
|
||||||
|
assert(serviceEventKind != null);
|
||||||
|
switch (serviceEventKind) {
|
||||||
|
case ServiceEvent.kPauseStart: message.write('paused (probably due to --start-paused)'); break;
|
||||||
|
case ServiceEvent.kPauseExit: message.write('paused because ${ plural ? 'they have' : 'it has' } terminated'); break;
|
||||||
|
case ServiceEvent.kPauseBreakpoint: message.write('paused in the debugger on a breakpoint'); break;
|
||||||
|
case ServiceEvent.kPauseInterrupted: message.write('paused due in the debugger'); break;
|
||||||
|
case ServiceEvent.kPauseException: message.write('paused in the debugger after an exception was thrown'); break;
|
||||||
|
case ServiceEvent.kPausePostRequest: message.write('paused'); break;
|
||||||
|
case '': message.write('paused for various reasons'); break;
|
||||||
|
default:
|
||||||
|
message.write('paused');
|
||||||
|
}
|
||||||
|
return message.toString();
|
||||||
|
}
|
||||||
|
|
||||||
bool _isPaused() {
|
bool _isPaused() {
|
||||||
for (FlutterDevice device in flutterDevices) {
|
for (FlutterDevice device in flutterDevices) {
|
||||||
for (FlutterView view in device.views) {
|
for (FlutterView view in device.views) {
|
||||||
@ -822,7 +909,6 @@ class HotRunner extends ResidentRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -482,19 +482,18 @@ abstract class FlutterCommand extends Command<void> {
|
|||||||
body: () async {
|
body: () async {
|
||||||
if (flutterUsage.isFirstRun)
|
if (flutterUsage.isFirstRun)
|
||||||
flutterUsage.printWelcome();
|
flutterUsage.printWelcome();
|
||||||
|
final String commandPath = await usagePath;
|
||||||
FlutterCommandResult commandResult;
|
FlutterCommandResult commandResult;
|
||||||
try {
|
try {
|
||||||
commandResult = await verifyThenRunCommand();
|
commandResult = await verifyThenRunCommand(commandPath);
|
||||||
} on ToolExit {
|
} on ToolExit {
|
||||||
commandResult = const FlutterCommandResult(ExitStatus.fail);
|
commandResult = const FlutterCommandResult(ExitStatus.fail);
|
||||||
rethrow;
|
rethrow;
|
||||||
} finally {
|
} finally {
|
||||||
final DateTime endTime = systemClock.now();
|
final DateTime endTime = systemClock.now();
|
||||||
printTrace(userMessages.flutterElapsedTime(name, getElapsedAsMilliseconds(endTime.difference(startTime))));
|
printTrace(userMessages.flutterElapsedTime(name, getElapsedAsMilliseconds(endTime.difference(startTime))));
|
||||||
// This is checking the result of the call to 'usagePath'
|
printTrace('"flutter $name" took ${getElapsedAsMilliseconds(endTime.difference(startTime))}.');
|
||||||
// (a Future<String>), and not the result of evaluating the Future.
|
if (commandPath != null) {
|
||||||
if (usagePath != null) {
|
|
||||||
final List<String> labels = <String>[];
|
final List<String> labels = <String>[];
|
||||||
if (commandResult?.exitStatus != null)
|
if (commandResult?.exitStatus != null)
|
||||||
labels.add(getEnumName(commandResult.exitStatus));
|
labels.add(getEnumName(commandResult.exitStatus));
|
||||||
@ -528,7 +527,7 @@ abstract class FlutterCommand extends Command<void> {
|
|||||||
/// then call this method to execute the command
|
/// then call this method to execute the command
|
||||||
/// rather than calling [runCommand] directly.
|
/// rather than calling [runCommand] directly.
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
Future<FlutterCommandResult> verifyThenRunCommand() async {
|
Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
|
||||||
await validateCommand();
|
await validateCommand();
|
||||||
|
|
||||||
// Populate the cache. We call this before pub get below so that the sky_engine
|
// Populate the cache. We call this before pub get below so that the sky_engine
|
||||||
@ -544,8 +543,6 @@ abstract class FlutterCommand extends Command<void> {
|
|||||||
|
|
||||||
setupApplicationPackages();
|
setupApplicationPackages();
|
||||||
|
|
||||||
final String commandPath = await usagePath;
|
|
||||||
|
|
||||||
if (commandPath != null) {
|
if (commandPath != null) {
|
||||||
final Map<String, String> additionalUsageValues = await usageValues;
|
final Map<String, String> additionalUsageValues = await usageValues;
|
||||||
flutterUsage.sendCommand(commandPath, parameters: additionalUsageValues);
|
flutterUsage.sendCommand(commandPath, parameters: additionalUsageValues);
|
||||||
|
@ -57,13 +57,7 @@ class CoverageCollector extends TestWatcher {
|
|||||||
if (result == null)
|
if (result == null)
|
||||||
throw Exception('Failed to collect coverage.');
|
throw Exception('Failed to collect coverage.');
|
||||||
data = result;
|
data = result;
|
||||||
})
|
});
|
||||||
.timeout(
|
|
||||||
const Duration(minutes: 10),
|
|
||||||
onTimeout: () {
|
|
||||||
throw Exception('Timed out while collecting coverage.');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await Future.any<void>(<Future<void>>[ processComplete, collectionComplete ]);
|
await Future.any<void>(<Future<void>>[ processComplete, collectionComplete ]);
|
||||||
assert(data != null);
|
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
|
/// This will not start any collection tasks. It us up to the caller of to
|
||||||
/// call [collectCoverage] for each process first.
|
/// call [collectCoverage] for each process first.
|
||||||
///
|
|
||||||
/// If [timeout] is specified, the future will timeout (with a
|
|
||||||
/// [TimeoutException]) after the specified duration.
|
|
||||||
Future<String> finalizeCoverage({
|
Future<String> finalizeCoverage({
|
||||||
coverage.Formatter formatter,
|
coverage.Formatter formatter,
|
||||||
Duration timeout,
|
|
||||||
Directory coverageDirectory,
|
Directory coverageDirectory,
|
||||||
}) async {
|
}) async {
|
||||||
printTrace('formating coverage data');
|
printTrace('formating coverage data');
|
||||||
@ -102,9 +92,8 @@ class CoverageCollector extends TestWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> collectCoverageData(String coveragePath, { bool mergeCoverageData = false, Directory coverageDirectory }) async {
|
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(
|
final String coverageData = await finalizeCoverage(
|
||||||
timeout: const Duration(seconds: 30),
|
|
||||||
coverageDirectory: coverageDirectory,
|
coverageDirectory: coverageDirectory,
|
||||||
);
|
);
|
||||||
status.stop();
|
status.stop();
|
||||||
|
@ -33,11 +33,17 @@ import 'watcher.dart';
|
|||||||
|
|
||||||
/// The timeout we give the test process to connect to the test harness
|
/// The timeout we give the test process to connect to the test harness
|
||||||
/// once the process has entered its main method.
|
/// once the process has entered its main method.
|
||||||
const Duration _kTestStartupTimeout = Duration(minutes: 1);
|
///
|
||||||
|
/// 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: 5);
|
||||||
|
|
||||||
/// The timeout we give the test process to start executing Dart code. When the
|
/// 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
|
/// 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.
|
/// any problem with Flutter, so we give it a large timeout.
|
||||||
|
///
|
||||||
|
/// See comment under [_kTestStartupTimeout] regarding timeouts.
|
||||||
const Duration _kTestProcessTimeout = Duration(minutes: 5);
|
const Duration _kTestProcessTimeout = Duration(minutes: 5);
|
||||||
|
|
||||||
/// Message logged by the test process to signal that its main method has begun
|
/// Message logged by the test process to signal that its main method has begun
|
||||||
@ -288,12 +294,10 @@ class _Compiler {
|
|||||||
firstCompile = true;
|
firstCompile = true;
|
||||||
}
|
}
|
||||||
suppressOutput = false;
|
suppressOutput = false;
|
||||||
final CompilerOutput compilerOutput = await handleTimeout<CompilerOutput>(
|
final CompilerOutput compilerOutput = await compiler.recompile(
|
||||||
compiler.recompile(
|
|
||||||
request.path,
|
request.path,
|
||||||
<String>[request.path],
|
<String>[request.path],
|
||||||
outputPath: outputDill.path),
|
outputPath: outputDill.path,
|
||||||
request.path,
|
|
||||||
);
|
);
|
||||||
final String outputPath = compilerOutput?.outputFilename;
|
final String outputPath = compilerOutput?.outputFilename;
|
||||||
|
|
||||||
@ -337,7 +341,7 @@ class _Compiler {
|
|||||||
Future<String> compile(String mainDart) {
|
Future<String> compile(String mainDart) {
|
||||||
final Completer<String> completer = Completer<String>();
|
final Completer<String> completer = Completer<String>();
|
||||||
compilerController.add(_CompilationRequest(mainDart, completer));
|
compilerController.add(_CompilationRequest(mainDart, completer));
|
||||||
return handleTimeout<String>(completer.future, mainDart);
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _shutdown() async {
|
Future<void> _shutdown() async {
|
||||||
@ -353,13 +357,6 @@ class _Compiler {
|
|||||||
await _shutdown();
|
await _shutdown();
|
||||||
await compilerController.close();
|
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 {
|
class _FlutterPlatform extends PlatformPlugin {
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'base/file_system.dart';
|
import 'base/file_system.dart';
|
||||||
|
import 'base/logger.dart';
|
||||||
import 'base/utils.dart';
|
import 'base/utils.dart';
|
||||||
import 'build_info.dart';
|
import 'build_info.dart';
|
||||||
import 'globals.dart';
|
import 'globals.dart';
|
||||||
@ -18,6 +19,8 @@ const String _kFirstUsefulFrameEventName = 'Widgets completed first useful frame
|
|||||||
class Tracing {
|
class Tracing {
|
||||||
Tracing(this.vmService);
|
Tracing(this.vmService);
|
||||||
|
|
||||||
|
static const String firstUsefulFrameEventName = _kFirstUsefulFrameEventName;
|
||||||
|
|
||||||
static Future<Tracing> connect(Uri uri) async {
|
static Future<Tracing> connect(Uri uri) async {
|
||||||
final VMService observatory = await VMService.connect(uri);
|
final VMService observatory = await VMService.connect(uri);
|
||||||
return Tracing(observatory);
|
return Tracing(observatory);
|
||||||
@ -32,17 +35,15 @@ class Tracing {
|
|||||||
|
|
||||||
/// Stops tracing; optionally wait for first frame.
|
/// Stops tracing; optionally wait for first frame.
|
||||||
Future<Map<String, dynamic>> stopTracingAndDownloadTimeline({
|
Future<Map<String, dynamic>> stopTracingAndDownloadTimeline({
|
||||||
bool waitForFirstFrame = false
|
bool awaitFirstFrame = false
|
||||||
}) async {
|
}) async {
|
||||||
Map<String, dynamic> timeline;
|
if (awaitFirstFrame) {
|
||||||
|
final Status status = logger.startProgress(
|
||||||
if (!waitForFirstFrame) {
|
'Waiting for application to render first frame...',
|
||||||
// Stop tracing immediately and get the timeline
|
timeout: kFastOperation,
|
||||||
await vmService.vm.setVMTimelineFlags(<String>[]);
|
);
|
||||||
timeline = await vmService.vm.getVMTimeline();
|
try {
|
||||||
} else {
|
|
||||||
final Completer<void> whenFirstFrameRendered = Completer<void>();
|
final Completer<void> whenFirstFrameRendered = Completer<void>();
|
||||||
|
|
||||||
(await vmService.onTimelineEvent).listen((ServiceEvent timelineEvent) {
|
(await vmService.onTimelineEvent).listen((ServiceEvent timelineEvent) {
|
||||||
final List<Map<String, dynamic>> events = timelineEvent.timelineEvents;
|
final List<Map<String, dynamic>> events = timelineEvent.timelineEvents;
|
||||||
for (Map<String, dynamic> event in events) {
|
for (Map<String, dynamic> event in events) {
|
||||||
@ -50,31 +51,30 @@ class Tracing {
|
|||||||
whenFirstFrameRendered.complete();
|
whenFirstFrameRendered.complete();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
bool done = false;
|
||||||
await whenFirstFrameRendered.future.timeout(
|
for (FlutterView view in vmService.vm.views) {
|
||||||
const Duration(seconds: 10),
|
if (await view.uiIsolate.flutterAlreadyPaintedFirstUsefulFrame()) {
|
||||||
onTimeout: () {
|
done = true;
|
||||||
printError(
|
break;
|
||||||
'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;
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
if (!done)
|
||||||
timeline = await vmService.vm.getVMTimeline();
|
await whenFirstFrameRendered.future;
|
||||||
|
} catch (exception) {
|
||||||
|
status.cancel();
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
status.stop();
|
||||||
|
}
|
||||||
|
final Map<String, dynamic> timeline = await vmService.vm.getVMTimeline();
|
||||||
await vmService.vm.setVMTimelineFlags(<String>[]);
|
await vmService.vm.setVMTimelineFlags(<String>[]);
|
||||||
}
|
|
||||||
|
|
||||||
return timeline;
|
return timeline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download the startup trace information from the given observatory client and
|
/// Download the startup trace information from the given observatory client and
|
||||||
/// store it to build/start_up_info.json.
|
/// store it to build/start_up_info.json.
|
||||||
Future<void> downloadStartupTrace(VMService observatory) async {
|
Future<void> downloadStartupTrace(VMService observatory, { bool awaitFirstFrame = true }) async {
|
||||||
final String traceInfoFilePath = fs.path.join(getBuildDirectory(), 'start_up_info.json');
|
final String traceInfoFilePath = fs.path.join(getBuildDirectory(), 'start_up_info.json');
|
||||||
final File traceInfoFile = fs.file(traceInfoFilePath);
|
final File traceInfoFile = fs.file(traceInfoFilePath);
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ Future<void> downloadStartupTrace(VMService observatory) async {
|
|||||||
final Tracing tracing = Tracing(observatory);
|
final Tracing tracing = Tracing(observatory);
|
||||||
|
|
||||||
final Map<String, dynamic> timeline = await tracing.stopTracingAndDownloadTimeline(
|
final Map<String, dynamic> timeline = await tracing.stopTracingAndDownloadTimeline(
|
||||||
waitForFirstFrame: true
|
awaitFirstFrame: awaitFirstFrame,
|
||||||
);
|
);
|
||||||
|
|
||||||
int extractInstantEventTimestamp(String eventName) {
|
int extractInstantEventTimestamp(String eventName) {
|
||||||
@ -101,33 +101,41 @@ Future<void> downloadStartupTrace(VMService observatory) async {
|
|||||||
return event == null ? null : event['ts'];
|
return event == null ? null : event['ts'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String message = 'No useful metrics were gathered.';
|
||||||
|
|
||||||
final int engineEnterTimestampMicros = extractInstantEventTimestamp(_kFlutterEngineMainEnterEventName);
|
final int engineEnterTimestampMicros = extractInstantEventTimestamp(_kFlutterEngineMainEnterEventName);
|
||||||
final int frameworkInitTimestampMicros = extractInstantEventTimestamp(_kFrameworkInitEventName);
|
final int frameworkInitTimestampMicros = extractInstantEventTimestamp(_kFrameworkInitEventName);
|
||||||
final int firstFrameTimestampMicros = extractInstantEventTimestamp(_kFirstUsefulFrameEventName);
|
|
||||||
|
|
||||||
if (engineEnterTimestampMicros == null) {
|
if (engineEnterTimestampMicros == null) {
|
||||||
printTrace('Engine start event is missing in the timeline: $timeline');
|
printTrace('Engine start event is missing in the timeline: $timeline');
|
||||||
throw 'Engine start event is missing in the timeline. Cannot compute startup time.';
|
throw 'Engine start event is missing in the timeline. Cannot compute startup time.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Map<String, dynamic> traceInfo = <String, dynamic>{
|
||||||
|
'engineEnterTimestampMicros': engineEnterTimestampMicros,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (frameworkInitTimestampMicros != null) {
|
||||||
|
final int timeToFrameworkInitMicros = frameworkInitTimestampMicros - engineEnterTimestampMicros;
|
||||||
|
traceInfo['timeToFrameworkInitMicros'] = timeToFrameworkInitMicros;
|
||||||
|
message = 'Time to framework init: ${timeToFrameworkInitMicros ~/ 1000}ms.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (awaitFirstFrame) {
|
||||||
|
final int firstFrameTimestampMicros = extractInstantEventTimestamp(_kFirstUsefulFrameEventName);
|
||||||
if (firstFrameTimestampMicros == null) {
|
if (firstFrameTimestampMicros == null) {
|
||||||
printTrace('First frame event is missing in the timeline: $timeline');
|
printTrace('First frame event is missing in the timeline: $timeline');
|
||||||
throw 'First frame event is missing in the timeline. Cannot compute startup time.';
|
throw 'First frame event is missing in the timeline. Cannot compute startup time.';
|
||||||
}
|
}
|
||||||
|
|
||||||
final int timeToFirstFrameMicros = firstFrameTimestampMicros - engineEnterTimestampMicros;
|
final int timeToFirstFrameMicros = firstFrameTimestampMicros - engineEnterTimestampMicros;
|
||||||
final Map<String, dynamic> traceInfo = <String, dynamic>{
|
traceInfo['timeToFirstFrameMicros'] = timeToFirstFrameMicros;
|
||||||
'engineEnterTimestampMicros': engineEnterTimestampMicros,
|
message = 'Time to first frame: ${timeToFirstFrameMicros ~/ 1000}ms.';
|
||||||
'timeToFirstFrameMicros': timeToFirstFrameMicros,
|
if (frameworkInitTimestampMicros != null)
|
||||||
};
|
|
||||||
|
|
||||||
if (frameworkInitTimestampMicros != null) {
|
|
||||||
traceInfo['timeToFrameworkInitMicros'] = frameworkInitTimestampMicros - engineEnterTimestampMicros;
|
|
||||||
traceInfo['timeAfterFrameworkInitMicros'] = firstFrameTimestampMicros - frameworkInitTimestampMicros;
|
traceInfo['timeAfterFrameworkInitMicros'] = firstFrameTimestampMicros - frameworkInitTimestampMicros;
|
||||||
}
|
}
|
||||||
|
|
||||||
traceInfoFile.writeAsStringSync(toPrettyJson(traceInfo));
|
traceInfoFile.writeAsStringSync(toPrettyJson(traceInfo));
|
||||||
|
|
||||||
printStatus('Time to first frame: ${timeToFirstFrameMicros ~/ 1000}ms.');
|
printStatus(message);
|
||||||
printStatus('Saved startup trace info in ${traceInfoFile.path}.');
|
printStatus('Saved startup trace info in ${traceInfoFile.path}.');
|
||||||
}
|
}
|
||||||
|
@ -15,12 +15,17 @@ import 'package:web_socket_channel/io.dart';
|
|||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
|
||||||
import 'base/common.dart';
|
import 'base/common.dart';
|
||||||
|
import 'base/context.dart';
|
||||||
import 'base/file_system.dart';
|
import 'base/file_system.dart';
|
||||||
import 'base/io.dart' as io;
|
import 'base/io.dart' as io;
|
||||||
import 'base/utils.dart';
|
import 'base/utils.dart';
|
||||||
import 'globals.dart';
|
import 'globals.dart';
|
||||||
import 'vmservice_record_replay.dart';
|
import 'vmservice_record_replay.dart';
|
||||||
|
|
||||||
|
/// Override `WebSocketConnector` in [context] to use a different constructor
|
||||||
|
/// for [WebSocket]s (used by tests).
|
||||||
|
typedef WebSocketConnector = Future<io.WebSocket> Function(String url);
|
||||||
|
|
||||||
/// A function that opens a two-way communication channel to the specified [uri].
|
/// A function that opens a two-way communication channel to the specified [uri].
|
||||||
typedef _OpenChannel = Future<StreamChannel<String>> Function(Uri uri);
|
typedef _OpenChannel = Future<StreamChannel<String>> Function(Uri uri);
|
||||||
|
|
||||||
@ -58,60 +63,46 @@ typedef CompileExpression = Future<String> Function(
|
|||||||
const String _kRecordingType = 'vmservice';
|
const String _kRecordingType = 'vmservice';
|
||||||
|
|
||||||
Future<StreamChannel<String>> _defaultOpenChannel(Uri uri) async {
|
Future<StreamChannel<String>> _defaultOpenChannel(Uri uri) async {
|
||||||
const int _kMaxAttempts = 5;
|
|
||||||
Duration delay = const Duration(milliseconds: 100);
|
Duration delay = const Duration(milliseconds: 100);
|
||||||
int attempts = 0;
|
int attempts = 0;
|
||||||
io.WebSocket socket;
|
io.WebSocket socket;
|
||||||
|
|
||||||
Future<void> onError(dynamic e) async {
|
Future<void> handleError(dynamic e) async {
|
||||||
printTrace('Exception attempting to connect to observatory: $e');
|
printTrace('Exception attempting to connect to Observatory: $e');
|
||||||
printTrace('This was attempt #$attempts. Will retry in $delay.');
|
printTrace('This was attempt #$attempts. Will retry in $delay.');
|
||||||
|
|
||||||
|
if (attempts == 10)
|
||||||
|
printStatus('This is taking longer than expected...');
|
||||||
|
|
||||||
// Delay next attempt.
|
// Delay next attempt.
|
||||||
await Future<void>.delayed(delay);
|
await Future<void>.delayed(delay);
|
||||||
|
|
||||||
// Back off exponentially.
|
// Back off exponentially, up to 1600ms per attempt.
|
||||||
|
if (delay < const Duration(seconds: 1))
|
||||||
delay *= 2;
|
delay *= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (attempts < _kMaxAttempts && socket == null) {
|
final WebSocketConnector constructor = context[WebSocketConnector] ?? io.WebSocket.connect;
|
||||||
|
while (socket == null) {
|
||||||
attempts += 1;
|
attempts += 1;
|
||||||
try {
|
try {
|
||||||
socket = await io.WebSocket.connect(uri.toString());
|
socket = await constructor(uri.toString());
|
||||||
} on io.WebSocketException catch (e) {
|
} on io.WebSocketException catch (e) {
|
||||||
await onError(e);
|
await handleError(e);
|
||||||
} on io.SocketException catch (e) {
|
} on io.SocketException catch (e) {
|
||||||
await onError(e);
|
await handleError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (socket == null) {
|
|
||||||
throw ToolExit(
|
|
||||||
'Attempted to connect to Dart observatory $_kMaxAttempts times, and '
|
|
||||||
'all attempts failed. Giving up. The URL was $uri'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return IOWebSocketChannel(socket).cast<String>();
|
return IOWebSocketChannel(socket).cast<String>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The default VM service request timeout.
|
|
||||||
const Duration kDefaultRequestTimeout = Duration(seconds: 30);
|
|
||||||
|
|
||||||
/// Used for RPC requests that may take a long time.
|
|
||||||
const Duration kLongRequestTimeout = Duration(minutes: 1);
|
|
||||||
|
|
||||||
/// Used for RPC requests that should never take a long time.
|
|
||||||
const Duration kShortRequestTimeout = Duration(seconds: 5);
|
|
||||||
|
|
||||||
// TODO(mklim): Test this, flutter/flutter#23031.
|
|
||||||
/// A connection to the Dart VM Service.
|
/// A connection to the Dart VM Service.
|
||||||
|
// TODO(mklim): Test this, https://github.com/flutter/flutter/issues/23031
|
||||||
class VMService {
|
class VMService {
|
||||||
VMService(
|
VMService(
|
||||||
this._peer,
|
this._peer,
|
||||||
this.httpAddress,
|
this.httpAddress,
|
||||||
this.wsAddress,
|
this.wsAddress,
|
||||||
this._requestTimeout,
|
|
||||||
ReloadSources reloadSources,
|
ReloadSources reloadSources,
|
||||||
Restart restart,
|
Restart restart,
|
||||||
CompileExpression compileExpression,
|
CompileExpression compileExpression,
|
||||||
@ -247,9 +238,6 @@ class VMService {
|
|||||||
|
|
||||||
/// Connect to a Dart VM Service at [httpUri].
|
/// Connect to a Dart VM Service at [httpUri].
|
||||||
///
|
///
|
||||||
/// Requests made via the returned [VMService] time out after [requestTimeout]
|
|
||||||
/// amount of time, which is [kDefaultRequestTimeout] by default.
|
|
||||||
///
|
|
||||||
/// If the [reloadSources] parameter is not null, the 'reloadSources' service
|
/// If the [reloadSources] parameter is not null, the 'reloadSources' service
|
||||||
/// will be registered. The VM Service Protocol allows clients to register
|
/// will be registered. The VM Service Protocol allows clients to register
|
||||||
/// custom services that can be invoked by other clients through the service
|
/// custom services that can be invoked by other clients through the service
|
||||||
@ -258,7 +246,6 @@ class VMService {
|
|||||||
/// See: https://github.com/dart-lang/sdk/commit/df8bf384eb815cf38450cb50a0f4b62230fba217
|
/// See: https://github.com/dart-lang/sdk/commit/df8bf384eb815cf38450cb50a0f4b62230fba217
|
||||||
static Future<VMService> connect(
|
static Future<VMService> connect(
|
||||||
Uri httpUri, {
|
Uri httpUri, {
|
||||||
Duration requestTimeout = kDefaultRequestTimeout,
|
|
||||||
ReloadSources reloadSources,
|
ReloadSources reloadSources,
|
||||||
Restart restart,
|
Restart restart,
|
||||||
CompileExpression compileExpression,
|
CompileExpression compileExpression,
|
||||||
@ -266,7 +253,7 @@ class VMService {
|
|||||||
final Uri wsUri = httpUri.replace(scheme: 'ws', path: fs.path.join(httpUri.path, 'ws'));
|
final Uri wsUri = httpUri.replace(scheme: 'ws', path: fs.path.join(httpUri.path, 'ws'));
|
||||||
final StreamChannel<String> channel = await _openChannel(wsUri);
|
final StreamChannel<String> channel = await _openChannel(wsUri);
|
||||||
final rpc.Peer peer = rpc.Peer.withoutJson(jsonDocument.bind(channel));
|
final rpc.Peer peer = rpc.Peer.withoutJson(jsonDocument.bind(channel));
|
||||||
final VMService service = VMService(peer, httpUri, wsUri, requestTimeout, reloadSources, restart, compileExpression);
|
final VMService service = VMService(peer, httpUri, wsUri, reloadSources, restart, compileExpression);
|
||||||
// This call is to ensure we are able to establish a connection instead of
|
// This call is to ensure we are able to establish a connection instead of
|
||||||
// keeping on trucking and failing farther down the process.
|
// keeping on trucking and failing farther down the process.
|
||||||
await service._sendRequest('getVersion', const <String, dynamic>{});
|
await service._sendRequest('getVersion', const <String, dynamic>{});
|
||||||
@ -276,7 +263,6 @@ class VMService {
|
|||||||
final Uri httpAddress;
|
final Uri httpAddress;
|
||||||
final Uri wsAddress;
|
final Uri wsAddress;
|
||||||
final rpc.Peer _peer;
|
final rpc.Peer _peer;
|
||||||
final Duration _requestTimeout;
|
|
||||||
final Completer<Map<String, dynamic>> _connectionError = Completer<Map<String, dynamic>>();
|
final Completer<Map<String, dynamic>> _connectionError = Completer<Map<String, dynamic>>();
|
||||||
|
|
||||||
VM _vm;
|
VM _vm;
|
||||||
@ -841,28 +827,15 @@ class VM extends ServiceObjectOwner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Invoke the RPC and return the raw response.
|
/// Invoke the RPC and return the raw response.
|
||||||
///
|
|
||||||
/// If `timeoutFatal` is false, then a timeout will result in a null return
|
|
||||||
/// value. Otherwise, it results in an exception.
|
|
||||||
Future<Map<String, dynamic>> invokeRpcRaw(String method, {
|
Future<Map<String, dynamic>> invokeRpcRaw(String method, {
|
||||||
Map<String, dynamic> params = const <String, dynamic>{},
|
Map<String, dynamic> params = const <String, dynamic>{},
|
||||||
Duration timeout,
|
|
||||||
bool timeoutFatal = true,
|
|
||||||
}) async {
|
}) async {
|
||||||
printTrace('Sending to VM service: $method($params)');
|
printTrace('Sending to VM service: $method($params)');
|
||||||
assert(params != null);
|
assert(params != null);
|
||||||
timeout ??= _vmService._requestTimeout;
|
|
||||||
try {
|
try {
|
||||||
final Map<String, dynamic> result = await _vmService
|
final Map<String, dynamic> result = await _vmService._sendRequest(method, params);
|
||||||
._sendRequest(method, params)
|
|
||||||
.timeout(timeout);
|
|
||||||
printTrace('Result: ${_truncate(result.toString(), 250, '...')}');
|
printTrace('Result: ${_truncate(result.toString(), 250, '...')}');
|
||||||
return result;
|
return result;
|
||||||
} on TimeoutException {
|
|
||||||
printTrace('Request to Dart VM Service timed out: $method($params)');
|
|
||||||
if (timeoutFatal)
|
|
||||||
throw TimeoutException('Request to Dart VM Service timed out: $method($params)');
|
|
||||||
return null;
|
|
||||||
} on WebSocketChannelException catch (error) {
|
} on WebSocketChannelException catch (error) {
|
||||||
throwToolExit('Error connecting to observatory: $error');
|
throwToolExit('Error connecting to observatory: $error');
|
||||||
return null;
|
return null;
|
||||||
@ -876,12 +849,10 @@ class VM extends ServiceObjectOwner {
|
|||||||
/// Invoke the RPC and return a [ServiceObject] response.
|
/// Invoke the RPC and return a [ServiceObject] response.
|
||||||
Future<T> invokeRpc<T extends ServiceObject>(String method, {
|
Future<T> invokeRpc<T extends ServiceObject>(String method, {
|
||||||
Map<String, dynamic> params = const <String, dynamic>{},
|
Map<String, dynamic> params = const <String, dynamic>{},
|
||||||
Duration timeout,
|
|
||||||
}) async {
|
}) async {
|
||||||
final Map<String, dynamic> response = await invokeRpcRaw(
|
final Map<String, dynamic> response = await invokeRpcRaw(
|
||||||
method,
|
method,
|
||||||
params: params,
|
params: params,
|
||||||
timeout: timeout,
|
|
||||||
);
|
);
|
||||||
final ServiceObject serviceObject = ServiceObject._fromMap(this, response);
|
final ServiceObject serviceObject = ServiceObject._fromMap(this, response);
|
||||||
if ((serviceObject != null) && (serviceObject._canCache)) {
|
if ((serviceObject != null) && (serviceObject._canCache)) {
|
||||||
@ -968,7 +939,7 @@ class VM extends ServiceObjectOwner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getVMTimeline() {
|
Future<Map<String, dynamic>> getVMTimeline() {
|
||||||
return invokeRpcRaw('_getVMTimeline', timeout: kLongRequestTimeout);
|
return invokeRpcRaw('_getVMTimeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshViews({ bool waitForViews = false }) async {
|
Future<void> refreshViews({ bool waitForViews = false }) async {
|
||||||
@ -982,10 +953,7 @@ class VM extends ServiceObjectOwner {
|
|||||||
// When the future returned by invokeRpc() below returns,
|
// When the future returned by invokeRpc() below returns,
|
||||||
// the _viewCache will have been updated.
|
// the _viewCache will have been updated.
|
||||||
// This message updates all the views of every isolate.
|
// This message updates all the views of every isolate.
|
||||||
await vmService.vm.invokeRpc<ServiceObject>(
|
await vmService.vm.invokeRpc<ServiceObject>('_flutter.listViews');
|
||||||
'_flutter.listViews',
|
|
||||||
timeout: kLongRequestTimeout,
|
|
||||||
);
|
|
||||||
if (_viewCache.values.isNotEmpty || !waitForViews)
|
if (_viewCache.values.isNotEmpty || !waitForViews)
|
||||||
return;
|
return;
|
||||||
failCount += 1;
|
failCount += 1;
|
||||||
@ -1124,8 +1092,6 @@ class Isolate extends ServiceObjectOwner {
|
|||||||
/// Invoke the RPC and return the raw response.
|
/// Invoke the RPC and return the raw response.
|
||||||
Future<Map<String, dynamic>> invokeRpcRaw(String method, {
|
Future<Map<String, dynamic>> invokeRpcRaw(String method, {
|
||||||
Map<String, dynamic> params,
|
Map<String, dynamic> params,
|
||||||
Duration timeout,
|
|
||||||
bool timeoutFatal = true,
|
|
||||||
}) {
|
}) {
|
||||||
// Inject the 'isolateId' parameter.
|
// Inject the 'isolateId' parameter.
|
||||||
if (params == null) {
|
if (params == null) {
|
||||||
@ -1135,7 +1101,7 @@ class Isolate extends ServiceObjectOwner {
|
|||||||
} else {
|
} else {
|
||||||
params['isolateId'] = id;
|
params['isolateId'] = id;
|
||||||
}
|
}
|
||||||
return vm.invokeRpcRaw(method, params: params, timeout: timeout, timeoutFatal: timeoutFatal);
|
return vm.invokeRpcRaw(method, params: params);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Invoke the RPC and return a ServiceObject response.
|
/// Invoke the RPC and return a ServiceObject response.
|
||||||
@ -1257,40 +1223,36 @@ class Isolate extends ServiceObjectOwner {
|
|||||||
Future<Map<String, dynamic>> invokeFlutterExtensionRpcRaw(
|
Future<Map<String, dynamic>> invokeFlutterExtensionRpcRaw(
|
||||||
String method, {
|
String method, {
|
||||||
Map<String, dynamic> params,
|
Map<String, dynamic> params,
|
||||||
Duration timeout,
|
|
||||||
bool timeoutFatal = true,
|
|
||||||
}
|
}
|
||||||
) {
|
) async {
|
||||||
return invokeRpcRaw(method, params: params, timeout: timeout,
|
try {
|
||||||
timeoutFatal: timeoutFatal).catchError((dynamic error) {
|
return await invokeRpcRaw(method, params: params);
|
||||||
if (error is rpc.RpcException) {
|
} on rpc.RpcException catch (e) {
|
||||||
// If an application is not using the framework
|
// If an application is not using the framework
|
||||||
if (error.code == rpc_error_code.METHOD_NOT_FOUND)
|
if (e.code == rpc_error_code.METHOD_NOT_FOUND)
|
||||||
return null;
|
return null;
|
||||||
throw error;
|
rethrow;
|
||||||
}});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug dump extension methods.
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>> flutterDebugDumpApp() {
|
Future<Map<String, dynamic>> flutterDebugDumpApp() {
|
||||||
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpApp', timeout: kLongRequestTimeout);
|
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpApp');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> flutterDebugDumpRenderTree() {
|
Future<Map<String, dynamic>> flutterDebugDumpRenderTree() {
|
||||||
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpRenderTree', timeout: kLongRequestTimeout);
|
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpRenderTree');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> flutterDebugDumpLayerTree() {
|
Future<Map<String, dynamic>> flutterDebugDumpLayerTree() {
|
||||||
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpLayerTree', timeout: kLongRequestTimeout);
|
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpLayerTree');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInTraversalOrder() {
|
Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInTraversalOrder() {
|
||||||
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInTraversalOrder', timeout: kLongRequestTimeout);
|
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInTraversalOrder');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInInverseHitTestOrder() {
|
Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInInverseHitTestOrder() {
|
||||||
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder', timeout: kLongRequestTimeout);
|
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> _flutterToggle(String name) async {
|
Future<Map<String, dynamic>> _flutterToggle(String name) async {
|
||||||
@ -1299,8 +1261,6 @@ class Isolate extends ServiceObjectOwner {
|
|||||||
state = await invokeFlutterExtensionRpcRaw(
|
state = await invokeFlutterExtensionRpcRaw(
|
||||||
'ext.flutter.$name',
|
'ext.flutter.$name',
|
||||||
params: <String, dynamic>{ 'enabled': state['enabled'] == 'true' ? 'false' : 'true' },
|
params: <String, dynamic>{ 'enabled': state['enabled'] == 'true' ? 'false' : 'true' },
|
||||||
timeout: const Duration(milliseconds: 150),
|
|
||||||
timeoutFatal: false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
@ -1312,22 +1272,20 @@ class Isolate extends ServiceObjectOwner {
|
|||||||
|
|
||||||
Future<Map<String, dynamic>> flutterToggleWidgetInspector() => _flutterToggle('inspector.show');
|
Future<Map<String, dynamic>> flutterToggleWidgetInspector() => _flutterToggle('inspector.show');
|
||||||
|
|
||||||
Future<void> flutterDebugAllowBanner(bool show) async {
|
Future<Map<String, dynamic>> flutterDebugAllowBanner(bool show) {
|
||||||
await invokeFlutterExtensionRpcRaw(
|
return invokeFlutterExtensionRpcRaw(
|
||||||
'ext.flutter.debugAllowBanner',
|
'ext.flutter.debugAllowBanner',
|
||||||
params: <String, dynamic>{ 'enabled': show ? 'true' : 'false' },
|
params: <String, dynamic>{ 'enabled': show ? 'true' : 'false' },
|
||||||
timeout: const Duration(milliseconds: 150),
|
|
||||||
timeoutFatal: false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload related extension methods.
|
|
||||||
Future<Map<String, dynamic>> flutterReassemble() {
|
Future<Map<String, dynamic>> flutterReassemble() {
|
||||||
return invokeFlutterExtensionRpcRaw(
|
return invokeFlutterExtensionRpcRaw('ext.flutter.reassemble');
|
||||||
'ext.flutter.reassemble',
|
}
|
||||||
timeout: kShortRequestTimeout,
|
|
||||||
timeoutFatal: true,
|
Future<bool> flutterAlreadyPaintedFirstUsefulFrame() async {
|
||||||
);
|
final Map<String, dynamic> result = await invokeFlutterExtensionRpcRaw('ext.flutter.didSendFirstFrameEvent');
|
||||||
|
return result['enabled'] == 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> uiWindowScheduleFrame() {
|
Future<Map<String, dynamic>> uiWindowScheduleFrame() {
|
||||||
@ -1335,7 +1293,8 @@ class Isolate extends ServiceObjectOwner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> flutterEvictAsset(String assetPath) {
|
Future<Map<String, dynamic>> flutterEvictAsset(String assetPath) {
|
||||||
return invokeFlutterExtensionRpcRaw('ext.flutter.evict',
|
return invokeFlutterExtensionRpcRaw(
|
||||||
|
'ext.flutter.evict',
|
||||||
params: <String, dynamic>{
|
params: <String, dynamic>{
|
||||||
'value': assetPath,
|
'value': assetPath,
|
||||||
}
|
}
|
||||||
@ -1352,19 +1311,13 @@ class Isolate extends ServiceObjectOwner {
|
|||||||
|
|
||||||
// Application control extension methods.
|
// Application control extension methods.
|
||||||
Future<Map<String, dynamic>> flutterExit() {
|
Future<Map<String, dynamic>> flutterExit() {
|
||||||
return invokeFlutterExtensionRpcRaw(
|
return invokeFlutterExtensionRpcRaw('ext.flutter.exit');
|
||||||
'ext.flutter.exit',
|
|
||||||
timeout: const Duration(seconds: 2),
|
|
||||||
timeoutFatal: false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> flutterPlatformOverride([String platform]) async {
|
Future<String> flutterPlatformOverride([String platform]) async {
|
||||||
final Map<String, dynamic> result = await invokeFlutterExtensionRpcRaw(
|
final Map<String, dynamic> result = await invokeFlutterExtensionRpcRaw(
|
||||||
'ext.flutter.platformOverride',
|
'ext.flutter.platformOverride',
|
||||||
params: platform != null ? <String, dynamic>{ 'value': platform } : <String, String>{},
|
params: platform != null ? <String, dynamic>{ 'value': platform } : <String, String>{},
|
||||||
timeout: const Duration(seconds: 5),
|
|
||||||
timeoutFatal: false,
|
|
||||||
);
|
);
|
||||||
if (result != null && result['value'] is String)
|
if (result != null && result['value'] is String)
|
||||||
return result['value'];
|
return result['value'];
|
||||||
|
@ -14,7 +14,6 @@ class VsCodeValidator extends DoctorValidator {
|
|||||||
|
|
||||||
final VsCode _vsCode;
|
final VsCode _vsCode;
|
||||||
|
|
||||||
|
|
||||||
static Iterable<DoctorValidator> get installedValidators {
|
static Iterable<DoctorValidator> get installedValidators {
|
||||||
return VsCode
|
return VsCode
|
||||||
.allInstalled()
|
.allInstalled()
|
||||||
|
@ -158,7 +158,7 @@ void main() {
|
|||||||
fallbacks: <Type, Generator>{
|
fallbacks: <Type, Generator>{
|
||||||
int: () => int.parse(context[String]),
|
int: () => int.parse(context[String]),
|
||||||
String: () => '${context[double]}',
|
String: () => '${context[double]}',
|
||||||
double: () => context[int] * 1.0,
|
double: () => (context[int] as int) * 1.0, // ignore: avoid_as
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
|
@ -2,13 +2,12 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter_tools/src/base/context.dart';
|
import 'package:flutter_tools/src/base/context.dart';
|
||||||
import 'package:flutter_tools/src/base/io.dart';
|
import 'package:flutter_tools/src/base/io.dart';
|
||||||
import 'package:flutter_tools/src/base/logger.dart';
|
import 'package:flutter_tools/src/base/logger.dart';
|
||||||
import 'package:flutter_tools/src/base/platform.dart';
|
import 'package:flutter_tools/src/base/platform.dart';
|
||||||
import 'package:flutter_tools/src/base/terminal.dart';
|
import 'package:flutter_tools/src/base/terminal.dart';
|
||||||
|
import 'package:quiver/testing/async.dart';
|
||||||
|
|
||||||
import '../src/common.dart';
|
import '../src/common.dart';
|
||||||
import '../src/context.dart';
|
import '../src/context.dart';
|
||||||
@ -64,43 +63,46 @@ void main() {
|
|||||||
|
|
||||||
group('Spinners', () {
|
group('Spinners', () {
|
||||||
MockStdio mockStdio;
|
MockStdio mockStdio;
|
||||||
AnsiSpinner ansiSpinner;
|
|
||||||
AnsiStatus ansiStatus;
|
AnsiStatus ansiStatus;
|
||||||
int called;
|
int called;
|
||||||
const List<String> testPlatforms = <String>['linux', 'macos', 'windows', 'fuchsia'];
|
const List<String> testPlatforms = <String>['linux', 'macos', 'windows', 'fuchsia'];
|
||||||
final RegExp secondDigits = RegExp(r'[^\b]\b\b\b\b\b[0-9]+[.][0-9]+(?:s|ms)');
|
final RegExp secondDigits = RegExp(r'[0-9,.]*[0-9]m?s');
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
mockStdio = MockStdio();
|
mockStdio = MockStdio();
|
||||||
ansiSpinner = AnsiSpinner();
|
|
||||||
called = 0;
|
called = 0;
|
||||||
ansiStatus = AnsiStatus(
|
ansiStatus = AnsiStatus(
|
||||||
message: 'Hello world',
|
message: 'Hello world',
|
||||||
expectSlowOperation: true,
|
timeout: const Duration(milliseconds: 10),
|
||||||
padding: 20,
|
padding: 20,
|
||||||
onFinish: () => called++,
|
onFinish: () => called += 1,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
List<String> outputStdout() => mockStdio.writtenToStdout.join('').split('\n');
|
List<String> outputStdout() => mockStdio.writtenToStdout.join('').split('\n');
|
||||||
List<String> outputStderr() => mockStdio.writtenToStderr.join('').split('\n');
|
List<String> outputStderr() => mockStdio.writtenToStderr.join('').split('\n');
|
||||||
|
|
||||||
Future<void> doWhileAsync(bool doThis()) async {
|
void doWhileAsync(FakeAsync time, bool doThis()) {
|
||||||
return Future.doWhile(() {
|
do {
|
||||||
// We want to let other tasks run at the same time, so we schedule these
|
time.elapse(const Duration(milliseconds: 1));
|
||||||
// using a timer rather than a microtask.
|
} while (doThis());
|
||||||
return Future<bool>.delayed(Duration.zero, doThis);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String testOs in testPlatforms) {
|
for (String testOs in testPlatforms) {
|
||||||
testUsingContext('AnsiSpinner works for $testOs', () async {
|
testUsingContext('AnsiSpinner works for $testOs', () async {
|
||||||
ansiSpinner.start();
|
bool done = false;
|
||||||
await doWhileAsync(() => ansiSpinner.ticks < 10);
|
FakeAsync().run((FakeAsync time) {
|
||||||
|
final AnsiSpinner ansiSpinner = AnsiSpinner(
|
||||||
|
timeout: const Duration(hours: 10),
|
||||||
|
)..start();
|
||||||
|
doWhileAsync(time, () => ansiSpinner.ticks < 10);
|
||||||
List<String> lines = outputStdout();
|
List<String> lines = outputStdout();
|
||||||
expect(lines[0], startsWith(platform.isWindows
|
expect(lines[0], startsWith(
|
||||||
? ' \b-\b\\\b|\b/\b-\b\\\b|\b/'
|
platform.isWindows
|
||||||
: ' \b⣾\b⣽\b⣻\b⢿\b⡿\b⣟\b⣯\b⣷\b⣾\b⣽'));
|
? ' \b\\\b|\b/\b-\b\\\b|\b/\b-'
|
||||||
|
: ' \b⣽\b⣻\b⢿\b⡿\b⣟\b⣯\b⣷\b⣾\b⣽\b⣻'
|
||||||
|
),
|
||||||
|
);
|
||||||
expect(lines[0].endsWith('\n'), isFalse);
|
expect(lines[0].endsWith('\n'), isFalse);
|
||||||
expect(lines.length, equals(1));
|
expect(lines.length, equals(1));
|
||||||
ansiSpinner.stop();
|
ansiSpinner.stop();
|
||||||
@ -115,22 +117,63 @@ void main() {
|
|||||||
expect(() {
|
expect(() {
|
||||||
ansiSpinner.cancel();
|
ansiSpinner.cancel();
|
||||||
}, throwsA(isInstanceOf<AssertionError>()));
|
}, throwsA(isInstanceOf<AssertionError>()));
|
||||||
|
done = true;
|
||||||
|
});
|
||||||
|
expect(done, isTrue);
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
Platform: () => FakePlatform(operatingSystem: testOs),
|
Platform: () => FakePlatform(operatingSystem: testOs),
|
||||||
Stdio: () => mockStdio,
|
Stdio: () => mockStdio,
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('Stdout startProgress handle null inputs on colored terminal for $testOs', () async {
|
testUsingContext('AnsiSpinner works for $testOs', () async {
|
||||||
context[Logger].startProgress(
|
bool done = false;
|
||||||
null,
|
// We pad the time here so that we have a little slack in terms of the first part of this test
|
||||||
|
// taking longer to run than we'd like, since we are forced to start the timer before the actual
|
||||||
|
// stopwatch that we're trying to test. This is an unfortunate possible race condition. If this
|
||||||
|
// turns out to be flaky, we will need to find another solution.
|
||||||
|
final Future<void> tenMillisecondsLater = Future<void>.delayed(const Duration(milliseconds: 15));
|
||||||
|
await FakeAsync().run((FakeAsync time) async {
|
||||||
|
final AnsiSpinner ansiSpinner = AnsiSpinner(
|
||||||
|
timeout: const Duration(milliseconds: 10),
|
||||||
|
)..start();
|
||||||
|
doWhileAsync(time, () => ansiSpinner.ticks < 10); // one second
|
||||||
|
expect(ansiSpinner.seemsSlow, isFalse); // ignore: invalid_use_of_protected_member
|
||||||
|
expect(outputStdout().join('\n'), isNot(contains('This is taking an unexpectedly long time.')));
|
||||||
|
await tenMillisecondsLater;
|
||||||
|
doWhileAsync(time, () => ansiSpinner.ticks < 30); // three seconds
|
||||||
|
expect(ansiSpinner.seemsSlow, isTrue); // ignore: invalid_use_of_protected_member
|
||||||
|
expect(outputStdout().join('\n'), contains('This is taking an unexpectedly long time.'));
|
||||||
|
ansiSpinner.stop();
|
||||||
|
expect(outputStdout().join('\n'), isNot(contains('(!)')));
|
||||||
|
done = true;
|
||||||
|
});
|
||||||
|
expect(done, isTrue);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
Platform: () => FakePlatform(operatingSystem: testOs),
|
||||||
|
Stdio: () => mockStdio,
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('Stdout startProgress on colored terminal for $testOs', () async {
|
||||||
|
bool done = false;
|
||||||
|
FakeAsync().run((FakeAsync time) {
|
||||||
|
final Logger logger = context[Logger];
|
||||||
|
final Status status = logger.startProgress(
|
||||||
|
'Hello',
|
||||||
progressId: null,
|
progressId: null,
|
||||||
expectSlowOperation: null,
|
timeout: kSlowOperation,
|
||||||
progressIndicatorPadding: null,
|
progressIndicatorPadding: 20, // this minus the "Hello" equals the 15 below.
|
||||||
);
|
);
|
||||||
final List<String> lines = outputStdout();
|
|
||||||
expect(outputStderr().length, equals(1));
|
expect(outputStderr().length, equals(1));
|
||||||
expect(outputStderr().first, isEmpty);
|
expect(outputStderr().first, isEmpty);
|
||||||
expect(lines[0], matches(platform.isWindows ? r'[ ]{64} [\b]-' : r'[ ]{64} [\b]⣾'));
|
// the 5 below is the margin that is always included between the message and the time.
|
||||||
|
expect(outputStdout().join('\n'), matches(platform.isWindows ? r'^Hello {15} {5} {8}[\b]{8} {7}\\$' :
|
||||||
|
r'^Hello {15} {5} {8}[\b]{8} {7}⣽$'));
|
||||||
|
status.stop();
|
||||||
|
expect(outputStdout().join('\n'), matches(platform.isWindows ? r'^Hello {15} {5} {8}[\b]{8} {7}\\[\b]{8} {8}[\b]{8}[\d, ]{4}[\d]\.[\d]s[\n]$' :
|
||||||
|
r'^Hello {15} {5} {8}[\b]{8} {7}⣽[\b]{8} {8}[\b]{8}[\d, ]{4}[\d]\.[\d]s[\n]$'));
|
||||||
|
done = true;
|
||||||
|
});
|
||||||
|
expect(done, isTrue);
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
Logger: () => StdoutLogger(),
|
Logger: () => StdoutLogger(),
|
||||||
OutputPreferences: () => OutputPreferences(showColor: true),
|
OutputPreferences: () => OutputPreferences(showColor: true),
|
||||||
@ -138,13 +181,82 @@ void main() {
|
|||||||
Stdio: () => mockStdio,
|
Stdio: () => mockStdio,
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('AnsiStatus works when cancelled for $testOs', () async {
|
testUsingContext('Stdout startProgress on colored terminal pauses on $testOs', () async {
|
||||||
|
bool done = false;
|
||||||
|
FakeAsync().run((FakeAsync time) {
|
||||||
|
final Logger logger = context[Logger];
|
||||||
|
final Status status = logger.startProgress(
|
||||||
|
'Knock Knock, Who\'s There',
|
||||||
|
timeout: const Duration(days: 10),
|
||||||
|
progressIndicatorPadding: 10,
|
||||||
|
);
|
||||||
|
logger.printStatus('Rude Interrupting Cow');
|
||||||
|
status.stop();
|
||||||
|
final String a = platform.isWindows ? '\\' : '⣽';
|
||||||
|
final String b = platform.isWindows ? '|' : '⣻';
|
||||||
|
expect(
|
||||||
|
outputStdout().join('\n'),
|
||||||
|
'Knock Knock, Who\'s There ' // initial message
|
||||||
|
' ' // placeholder so that spinner can backspace on its first tick
|
||||||
|
'\b\b\b\b\b\b\b\b $a' // first tick
|
||||||
|
'\b\b\b\b\b\b\b\b ' // clearing the spinner
|
||||||
|
'\b\b\b\b\b\b\b\b' // clearing the clearing of the spinner
|
||||||
|
'\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b ' // clearing the message
|
||||||
|
'\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b' // clearing the clearing of the message
|
||||||
|
'Rude Interrupting Cow\n' // message
|
||||||
|
'Knock Knock, Who\'s There ' // message restoration
|
||||||
|
' ' // placeholder so that spinner can backspace on its second tick
|
||||||
|
'\b\b\b\b\b\b\b\b $b' // second tick
|
||||||
|
'\b\b\b\b\b\b\b\b ' // clearing the spinner to put the time
|
||||||
|
'\b\b\b\b\b\b\b\b' // clearing the clearing of the spinner
|
||||||
|
' 0.0s\n', // replacing it with the time
|
||||||
|
);
|
||||||
|
done = true;
|
||||||
|
});
|
||||||
|
expect(done, isTrue);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
Logger: () => StdoutLogger(),
|
||||||
|
OutputPreferences: () => OutputPreferences(showColor: true),
|
||||||
|
Platform: () => FakePlatform(operatingSystem: testOs)..stdoutSupportsAnsi = true,
|
||||||
|
Stdio: () => mockStdio,
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('AnsiStatus works for $testOs', () async {
|
||||||
|
bool done = false;
|
||||||
|
// We pad the time here so that we have a little slack in terms of the first part of this test
|
||||||
|
// taking longer to run than we'd like, since we are forced to start the timer before the actual
|
||||||
|
// stopwatch that we're trying to test. This is an unfortunate possible race condition. If this
|
||||||
|
// turns out to be flaky, we will need to find another solution.
|
||||||
|
final Future<void> tenMillisecondsLater = Future<void>.delayed(const Duration(milliseconds: 15));
|
||||||
|
await FakeAsync().run((FakeAsync time) async {
|
||||||
ansiStatus.start();
|
ansiStatus.start();
|
||||||
await doWhileAsync(() => ansiStatus.ticks < 10);
|
doWhileAsync(time, () => ansiStatus.ticks < 10); // one second
|
||||||
|
expect(ansiStatus.seemsSlow, isFalse); // ignore: invalid_use_of_protected_member
|
||||||
|
expect(outputStdout().join('\n'), isNot(contains('This is taking an unexpectedly long time.')));
|
||||||
|
expect(outputStdout().join('\n'), isNot(contains('(!)')));
|
||||||
|
await tenMillisecondsLater;
|
||||||
|
doWhileAsync(time, () => ansiStatus.ticks < 30); // three seconds
|
||||||
|
expect(ansiStatus.seemsSlow, isTrue); // ignore: invalid_use_of_protected_member
|
||||||
|
expect(outputStdout().join('\n'), contains('This is taking an unexpectedly long time.'));
|
||||||
|
ansiStatus.stop();
|
||||||
|
expect(outputStdout().join('\n'), contains('(!)'));
|
||||||
|
done = true;
|
||||||
|
});
|
||||||
|
expect(done, isTrue);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
Platform: () => FakePlatform(operatingSystem: testOs),
|
||||||
|
Stdio: () => mockStdio,
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('AnsiStatus works when cancelled for $testOs', () async {
|
||||||
|
bool done = false;
|
||||||
|
FakeAsync().run((FakeAsync time) {
|
||||||
|
ansiStatus.start();
|
||||||
|
doWhileAsync(time, () => ansiStatus.ticks < 10);
|
||||||
List<String> lines = outputStdout();
|
List<String> lines = outputStdout();
|
||||||
expect(lines[0], startsWith(platform.isWindows
|
expect(lines[0], startsWith(platform.isWindows
|
||||||
? 'Hello world \b-\b\\\b|\b/\b-\b\\\b|\b/'
|
? 'Hello world \b\b\b\b\b\b\b\b \\\b\b\b\b\b\b\b\b |\b\b\b\b\b\b\b\b /\b\b\b\b\b\b\b\b -\b\b\b\b\b\b\b\b \\\b\b\b\b\b\b\b\b |\b\b\b\b\b\b\b\b /\b\b\b\b\b\b\b\b -\b\b\b\b\b\b\b\b \\\b\b\b\b\b\b\b\b |'
|
||||||
: 'Hello world \b⣾\b⣽\b⣻\b⢿\b⡿\b⣟\b⣯\b⣷\b⣾\b⣽'));
|
: 'Hello world \b\b\b\b\b\b\b\b ⣽\b\b\b\b\b\b\b\b ⣻\b\b\b\b\b\b\b\b ⢿\b\b\b\b\b\b\b\b ⡿\b\b\b\b\b\b\b\b ⣟\b\b\b\b\b\b\b\b ⣯\b\b\b\b\b\b\b\b ⣷\b\b\b\b\b\b\b\b ⣾\b\b\b\b\b\b\b\b ⣽\b\b\b\b\b\b\b\b ⣻'));
|
||||||
expect(lines.length, equals(1));
|
expect(lines.length, equals(1));
|
||||||
expect(lines[0].endsWith('\n'), isFalse);
|
expect(lines[0].endsWith('\n'), isFalse);
|
||||||
|
|
||||||
@ -153,7 +265,8 @@ void main() {
|
|||||||
lines = outputStdout();
|
lines = outputStdout();
|
||||||
final List<Match> matches = secondDigits.allMatches(lines[0]).toList();
|
final List<Match> matches = secondDigits.allMatches(lines[0]).toList();
|
||||||
expect(matches, isEmpty);
|
expect(matches, isEmpty);
|
||||||
expect(lines[0], endsWith('\b \b'));
|
final String x = platform.isWindows ? '|' : '⣻';
|
||||||
|
expect(lines[0], endsWith('$x\b\b\b\b\b\b\b\b \b\b\b\b\b\b\b\b'));
|
||||||
expect(called, equals(1));
|
expect(called, equals(1));
|
||||||
expect(lines.length, equals(2));
|
expect(lines.length, equals(2));
|
||||||
expect(lines[1], equals(''));
|
expect(lines[1], equals(''));
|
||||||
@ -161,27 +274,41 @@ void main() {
|
|||||||
// Verify that stopping or canceling multiple times throws.
|
// Verify that stopping or canceling multiple times throws.
|
||||||
expect(() { ansiStatus.cancel(); }, throwsA(isInstanceOf<AssertionError>()));
|
expect(() { ansiStatus.cancel(); }, throwsA(isInstanceOf<AssertionError>()));
|
||||||
expect(() { ansiStatus.stop(); }, throwsA(isInstanceOf<AssertionError>()));
|
expect(() { ansiStatus.stop(); }, throwsA(isInstanceOf<AssertionError>()));
|
||||||
|
done = true;
|
||||||
|
});
|
||||||
|
expect(done, isTrue);
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
Platform: () => FakePlatform(operatingSystem: testOs),
|
Platform: () => FakePlatform(operatingSystem: testOs),
|
||||||
Stdio: () => mockStdio,
|
Stdio: () => mockStdio,
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('AnsiStatus works when stopped for $testOs', () async {
|
testUsingContext('AnsiStatus works when stopped for $testOs', () async {
|
||||||
|
bool done = false;
|
||||||
|
FakeAsync().run((FakeAsync time) {
|
||||||
ansiStatus.start();
|
ansiStatus.start();
|
||||||
await doWhileAsync(() => ansiStatus.ticks < 10);
|
doWhileAsync(time, () => ansiStatus.ticks < 10);
|
||||||
List<String> lines = outputStdout();
|
List<String> lines = outputStdout();
|
||||||
expect(lines[0], startsWith(platform.isWindows
|
expect(lines, hasLength(1));
|
||||||
? 'Hello world \b-\b\\\b|\b/\b-\b\\\b|\b/'
|
expect(lines[0],
|
||||||
: 'Hello world \b⣾\b⣽\b⣻\b⢿\b⡿\b⣟\b⣯\b⣷\b⣾\b⣽'));
|
platform.isWindows
|
||||||
expect(lines.length, equals(1));
|
? 'Hello world \b\b\b\b\b\b\b\b \\\b\b\b\b\b\b\b\b |\b\b\b\b\b\b\b\b /\b\b\b\b\b\b\b\b -\b\b\b\b\b\b\b\b \\\b\b\b\b\b\b\b\b |\b\b\b\b\b\b\b\b /\b\b\b\b\b\b\b\b -\b\b\b\b\b\b\b\b \\\b\b\b\b\b\b\b\b |'
|
||||||
|
: 'Hello world \b\b\b\b\b\b\b\b ⣽\b\b\b\b\b\b\b\b ⣻\b\b\b\b\b\b\b\b ⢿\b\b\b\b\b\b\b\b ⡿\b\b\b\b\b\b\b\b ⣟\b\b\b\b\b\b\b\b ⣯\b\b\b\b\b\b\b\b ⣷\b\b\b\b\b\b\b\b ⣾\b\b\b\b\b\b\b\b ⣽\b\b\b\b\b\b\b\b ⣻',
|
||||||
|
);
|
||||||
|
|
||||||
// Verify a stop prints the time.
|
// Verify a stop prints the time.
|
||||||
ansiStatus.stop();
|
ansiStatus.stop();
|
||||||
lines = outputStdout();
|
lines = outputStdout();
|
||||||
final List<Match> matches = secondDigits.allMatches(lines[0]).toList();
|
expect(lines, hasLength(2));
|
||||||
expect(matches, isNotNull);
|
expect(lines[0], matches(
|
||||||
expect(matches, hasLength(1));
|
platform.isWindows
|
||||||
final Match match = matches.first;
|
? r'Hello world {8}[\b]{8} {7}\\[\b]{8} {7}|[\b]{8} {7}/[\b]{8} {7}-[\b]{8} {7}\\[\b]{8} {7}|[\b]{8} {7}/[\b]{8} {7}-[\b]{8} {7}\\[\b]{8} {7}|[\b]{8} {7} [\b]{8}[\d., ]{6}[\d]ms$'
|
||||||
|
: r'Hello world {8}[\b]{8} {7}⣽[\b]{8} {7}⣻[\b]{8} {7}⢿[\b]{8} {7}⡿[\b]{8} {7}⣟[\b]{8} {7}⣯[\b]{8} {7}⣷[\b]{8} {7}⣾[\b]{8} {7}⣽[\b]{8} {7}⣻[\b]{8} {7} [\b]{8}[\d., ]{5}[\d]ms$'
|
||||||
|
));
|
||||||
|
expect(lines[1], isEmpty);
|
||||||
|
final List<Match> times = secondDigits.allMatches(lines[0]).toList();
|
||||||
|
expect(times, isNotNull);
|
||||||
|
expect(times, hasLength(1));
|
||||||
|
final Match match = times.single;
|
||||||
expect(lines[0], endsWith(match.group(0)));
|
expect(lines[0], endsWith(match.group(0)));
|
||||||
expect(called, equals(1));
|
expect(called, equals(1));
|
||||||
expect(lines.length, equals(2));
|
expect(lines.length, equals(2));
|
||||||
@ -190,6 +317,9 @@ void main() {
|
|||||||
// Verify that stopping or canceling multiple times throws.
|
// Verify that stopping or canceling multiple times throws.
|
||||||
expect(() { ansiStatus.stop(); }, throwsA(isInstanceOf<AssertionError>()));
|
expect(() { ansiStatus.stop(); }, throwsA(isInstanceOf<AssertionError>()));
|
||||||
expect(() { ansiStatus.cancel(); }, throwsA(isInstanceOf<AssertionError>()));
|
expect(() { ansiStatus.cancel(); }, throwsA(isInstanceOf<AssertionError>()));
|
||||||
|
done = true;
|
||||||
|
});
|
||||||
|
expect(done, isTrue);
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
Platform: () => FakePlatform(operatingSystem: testOs),
|
Platform: () => FakePlatform(operatingSystem: testOs),
|
||||||
Stdio: () => mockStdio,
|
Stdio: () => mockStdio,
|
||||||
@ -207,7 +337,7 @@ void main() {
|
|||||||
called = 0;
|
called = 0;
|
||||||
summaryStatus = SummaryStatus(
|
summaryStatus = SummaryStatus(
|
||||||
message: 'Hello world',
|
message: 'Hello world',
|
||||||
expectSlowOperation: true,
|
timeout: kSlowOperation,
|
||||||
padding: 20,
|
padding: 20,
|
||||||
onFinish: () => called++,
|
onFinish: () => called++,
|
||||||
);
|
);
|
||||||
@ -217,7 +347,8 @@ void main() {
|
|||||||
List<String> outputStderr() => mockStdio.writtenToStderr.join('').split('\n');
|
List<String> outputStderr() => mockStdio.writtenToStderr.join('').split('\n');
|
||||||
|
|
||||||
testUsingContext('Error logs are wrapped', () async {
|
testUsingContext('Error logs are wrapped', () async {
|
||||||
context[Logger].printError('0123456789' * 15);
|
final Logger logger = context[Logger];
|
||||||
|
logger.printError('0123456789' * 15);
|
||||||
final List<String> lines = outputStderr();
|
final List<String> lines = outputStderr();
|
||||||
expect(outputStdout().length, equals(1));
|
expect(outputStdout().length, equals(1));
|
||||||
expect(outputStdout().first, isEmpty);
|
expect(outputStdout().first, isEmpty);
|
||||||
@ -233,7 +364,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('Error logs are wrapped and can be indented.', () async {
|
testUsingContext('Error logs are wrapped and can be indented.', () async {
|
||||||
context[Logger].printError('0123456789' * 15, indent: 5);
|
final Logger logger = context[Logger];
|
||||||
|
logger.printError('0123456789' * 15, indent: 5);
|
||||||
final List<String> lines = outputStderr();
|
final List<String> lines = outputStderr();
|
||||||
expect(outputStdout().length, equals(1));
|
expect(outputStdout().length, equals(1));
|
||||||
expect(outputStdout().first, isEmpty);
|
expect(outputStdout().first, isEmpty);
|
||||||
@ -252,7 +384,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('Error logs are wrapped and can have hanging indent.', () async {
|
testUsingContext('Error logs are wrapped and can have hanging indent.', () async {
|
||||||
context[Logger].printError('0123456789' * 15, hangingIndent: 5);
|
final Logger logger = context[Logger];
|
||||||
|
logger.printError('0123456789' * 15, hangingIndent: 5);
|
||||||
final List<String> lines = outputStderr();
|
final List<String> lines = outputStderr();
|
||||||
expect(outputStdout().length, equals(1));
|
expect(outputStdout().length, equals(1));
|
||||||
expect(outputStdout().first, isEmpty);
|
expect(outputStdout().first, isEmpty);
|
||||||
@ -271,7 +404,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('Error logs are wrapped, indented, and can have hanging indent.', () async {
|
testUsingContext('Error logs are wrapped, indented, and can have hanging indent.', () async {
|
||||||
context[Logger].printError('0123456789' * 15, indent: 4, hangingIndent: 5);
|
final Logger logger = context[Logger];
|
||||||
|
logger.printError('0123456789' * 15, indent: 4, hangingIndent: 5);
|
||||||
final List<String> lines = outputStderr();
|
final List<String> lines = outputStderr();
|
||||||
expect(outputStdout().length, equals(1));
|
expect(outputStdout().length, equals(1));
|
||||||
expect(outputStdout().first, isEmpty);
|
expect(outputStdout().first, isEmpty);
|
||||||
@ -290,7 +424,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('Stdout logs are wrapped', () async {
|
testUsingContext('Stdout logs are wrapped', () async {
|
||||||
context[Logger].printStatus('0123456789' * 15);
|
final Logger logger = context[Logger];
|
||||||
|
logger.printStatus('0123456789' * 15);
|
||||||
final List<String> lines = outputStdout();
|
final List<String> lines = outputStdout();
|
||||||
expect(outputStderr().length, equals(1));
|
expect(outputStderr().length, equals(1));
|
||||||
expect(outputStderr().first, isEmpty);
|
expect(outputStderr().first, isEmpty);
|
||||||
@ -306,7 +441,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('Stdout logs are wrapped and can be indented.', () async {
|
testUsingContext('Stdout logs are wrapped and can be indented.', () async {
|
||||||
context[Logger].printStatus('0123456789' * 15, indent: 5);
|
final Logger logger = context[Logger];
|
||||||
|
logger.printStatus('0123456789' * 15, indent: 5);
|
||||||
final List<String> lines = outputStdout();
|
final List<String> lines = outputStdout();
|
||||||
expect(outputStderr().length, equals(1));
|
expect(outputStderr().length, equals(1));
|
||||||
expect(outputStderr().first, isEmpty);
|
expect(outputStderr().first, isEmpty);
|
||||||
@ -325,7 +461,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('Stdout logs are wrapped and can have hanging indent.', () async {
|
testUsingContext('Stdout logs are wrapped and can have hanging indent.', () async {
|
||||||
context[Logger].printStatus('0123456789' * 15, hangingIndent: 5);
|
final Logger logger = context[Logger];
|
||||||
|
logger.printStatus('0123456789' * 15, hangingIndent: 5);
|
||||||
final List<String> lines = outputStdout();
|
final List<String> lines = outputStdout();
|
||||||
expect(outputStderr().length, equals(1));
|
expect(outputStderr().length, equals(1));
|
||||||
expect(outputStderr().first, isEmpty);
|
expect(outputStderr().first, isEmpty);
|
||||||
@ -344,7 +481,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('Stdout logs are wrapped, indented, and can have hanging indent.', () async {
|
testUsingContext('Stdout logs are wrapped, indented, and can have hanging indent.', () async {
|
||||||
context[Logger].printStatus('0123456789' * 15, indent: 4, hangingIndent: 5);
|
final Logger logger = context[Logger];
|
||||||
|
logger.printStatus('0123456789' * 15, indent: 4, hangingIndent: 5);
|
||||||
final List<String> lines = outputStdout();
|
final List<String> lines = outputStdout();
|
||||||
expect(outputStderr().length, equals(1));
|
expect(outputStderr().length, equals(1));
|
||||||
expect(outputStderr().first, isEmpty);
|
expect(outputStderr().first, isEmpty);
|
||||||
@ -363,7 +501,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('Error logs are red', () async {
|
testUsingContext('Error logs are red', () async {
|
||||||
context[Logger].printError('Pants on fire!');
|
final Logger logger = context[Logger];
|
||||||
|
logger.printError('Pants on fire!');
|
||||||
final List<String> lines = outputStderr();
|
final List<String> lines = outputStderr();
|
||||||
expect(outputStdout().length, equals(1));
|
expect(outputStdout().length, equals(1));
|
||||||
expect(outputStdout().first, isEmpty);
|
expect(outputStdout().first, isEmpty);
|
||||||
@ -376,7 +515,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('Stdout logs are not colored', () async {
|
testUsingContext('Stdout logs are not colored', () async {
|
||||||
context[Logger].printStatus('All good.');
|
final Logger logger = context[Logger];
|
||||||
|
logger.printStatus('All good.');
|
||||||
final List<String> lines = outputStdout();
|
final List<String> lines = outputStdout();
|
||||||
expect(outputStderr().length, equals(1));
|
expect(outputStderr().length, equals(1));
|
||||||
expect(outputStderr().first, isEmpty);
|
expect(outputStderr().first, isEmpty);
|
||||||
@ -388,7 +528,14 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('Stdout printStatus handle null inputs on colored terminal', () async {
|
testUsingContext('Stdout printStatus handle null inputs on colored terminal', () async {
|
||||||
context[Logger].printStatus(null, emphasis: null, color: null, newline: null, indent: null);
|
final Logger logger = context[Logger];
|
||||||
|
logger.printStatus(
|
||||||
|
null,
|
||||||
|
emphasis: null,
|
||||||
|
color: null,
|
||||||
|
newline: null,
|
||||||
|
indent: null,
|
||||||
|
);
|
||||||
final List<String> lines = outputStdout();
|
final List<String> lines = outputStdout();
|
||||||
expect(outputStderr().length, equals(1));
|
expect(outputStderr().length, equals(1));
|
||||||
expect(outputStderr().first, isEmpty);
|
expect(outputStderr().first, isEmpty);
|
||||||
@ -399,8 +546,15 @@ void main() {
|
|||||||
Stdio: () => mockStdio,
|
Stdio: () => mockStdio,
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('Stdout printStatus handle null inputs on regular terminal', () async {
|
testUsingContext('Stdout printStatus handle null inputs on non-color terminal', () async {
|
||||||
context[Logger].printStatus(null, emphasis: null, color: null, newline: null, indent: null);
|
final Logger logger = context[Logger];
|
||||||
|
logger.printStatus(
|
||||||
|
null,
|
||||||
|
emphasis: null,
|
||||||
|
color: null,
|
||||||
|
newline: null,
|
||||||
|
indent: null,
|
||||||
|
);
|
||||||
final List<String> lines = outputStdout();
|
final List<String> lines = outputStdout();
|
||||||
expect(outputStderr().length, equals(1));
|
expect(outputStderr().length, equals(1));
|
||||||
expect(outputStderr().first, isEmpty);
|
expect(outputStderr().first, isEmpty);
|
||||||
@ -412,17 +566,27 @@ void main() {
|
|||||||
Platform: _kNoAnsiPlatform,
|
Platform: _kNoAnsiPlatform,
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('Stdout startProgress handle null inputs on regular terminal', () async {
|
testUsingContext('Stdout startProgress on non-color terminal', () async {
|
||||||
context[Logger].startProgress(
|
bool done = false;
|
||||||
null,
|
FakeAsync().run((FakeAsync time) {
|
||||||
|
final Logger logger = context[Logger];
|
||||||
|
final Status status = logger.startProgress(
|
||||||
|
'Hello',
|
||||||
progressId: null,
|
progressId: null,
|
||||||
expectSlowOperation: null,
|
timeout: kSlowOperation,
|
||||||
progressIndicatorPadding: null,
|
progressIndicatorPadding: 20, // this minus the "Hello" equals the 15 below.
|
||||||
);
|
);
|
||||||
final List<String> lines = outputStdout();
|
|
||||||
expect(outputStderr().length, equals(1));
|
expect(outputStderr().length, equals(1));
|
||||||
expect(outputStderr().first, isEmpty);
|
expect(outputStderr().first, isEmpty);
|
||||||
expect(lines[0], matches('[ ]{64}'));
|
// the 5 below is the margin that is always included between the message and the time.
|
||||||
|
expect(outputStdout().join('\n'), matches(platform.isWindows ? r'^Hello {15} {5}$' :
|
||||||
|
r'^Hello {15} {5}$'));
|
||||||
|
status.stop();
|
||||||
|
expect(outputStdout().join('\n'), matches(platform.isWindows ? r'^Hello {15} {5}[\d, ]{4}[\d]\.[\d]s[\n]$' :
|
||||||
|
r'^Hello {15} {5}[\d, ]{4}[\d]\.[\d]s[\n]$'));
|
||||||
|
done = true;
|
||||||
|
});
|
||||||
|
expect(done, isTrue);
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
Logger: () => StdoutLogger(),
|
Logger: () => StdoutLogger(),
|
||||||
OutputPreferences: () => OutputPreferences(showColor: false),
|
OutputPreferences: () => OutputPreferences(showColor: false),
|
||||||
@ -476,11 +640,16 @@ void main() {
|
|||||||
}, overrides: <Type, Generator>{Stdio: () => mockStdio, Platform: _kNoAnsiPlatform});
|
}, overrides: <Type, Generator>{Stdio: () => mockStdio, Platform: _kNoAnsiPlatform});
|
||||||
|
|
||||||
testUsingContext('sequential startProgress calls with StdoutLogger', () async {
|
testUsingContext('sequential startProgress calls with StdoutLogger', () async {
|
||||||
context[Logger].startProgress('AAA')..stop();
|
final Logger logger = context[Logger];
|
||||||
context[Logger].startProgress('BBB')..stop();
|
logger.startProgress('AAA', timeout: kFastOperation)..stop();
|
||||||
expect(outputStdout().length, equals(3));
|
logger.startProgress('BBB', timeout: kFastOperation)..stop();
|
||||||
expect(outputStdout()[0], matches(RegExp(r'AAA[ ]{60}[\d ]{3}[\d]ms')));
|
final List<String> output = outputStdout();
|
||||||
expect(outputStdout()[1], matches(RegExp(r'BBB[ ]{60}[\d ]{3}[\d]ms')));
|
expect(output.length, equals(3));
|
||||||
|
// There's 61 spaces at the start: 59 (padding default) - 3 (length of AAA) + 5 (margin).
|
||||||
|
// Then there's a left-padded "0ms" 8 characters wide, so 5 spaces then "0ms"
|
||||||
|
// (except sometimes it's randomly slow so we handle up to "99,999ms").
|
||||||
|
expect(output[0], matches(RegExp(r'AAA[ ]{61}[\d, ]{5}[\d]ms')));
|
||||||
|
expect(output[1], matches(RegExp(r'BBB[ ]{61}[\d, ]{5}[\d]ms')));
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
Logger: () => StdoutLogger(),
|
Logger: () => StdoutLogger(),
|
||||||
OutputPreferences: () => OutputPreferences(showColor: false),
|
OutputPreferences: () => OutputPreferences(showColor: false),
|
||||||
@ -489,13 +658,14 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('sequential startProgress calls with VerboseLogger and StdoutLogger', () async {
|
testUsingContext('sequential startProgress calls with VerboseLogger and StdoutLogger', () async {
|
||||||
context[Logger].startProgress('AAA')..stop();
|
final Logger logger = context[Logger];
|
||||||
context[Logger].startProgress('BBB')..stop();
|
logger.startProgress('AAA', timeout: kFastOperation)..stop();
|
||||||
|
logger.startProgress('BBB', timeout: kFastOperation)..stop();
|
||||||
expect(outputStdout(), <Matcher>[
|
expect(outputStdout(), <Matcher>[
|
||||||
matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] AAA$'),
|
matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] AAA$'),
|
||||||
matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] AAA \(completed\)$'),
|
matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] AAA \(completed.*\)$'),
|
||||||
matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] BBB$'),
|
matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] BBB$'),
|
||||||
matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] BBB \(completed\)$'),
|
matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] BBB \(completed.*\)$'),
|
||||||
matches(r'^$'),
|
matches(r'^$'),
|
||||||
]);
|
]);
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
@ -505,9 +675,9 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('sequential startProgress calls with BufferLogger', () async {
|
testUsingContext('sequential startProgress calls with BufferLogger', () async {
|
||||||
context[Logger].startProgress('AAA')..stop();
|
|
||||||
context[Logger].startProgress('BBB')..stop();
|
|
||||||
final BufferLogger logger = context[Logger];
|
final BufferLogger logger = context[Logger];
|
||||||
|
logger.startProgress('AAA', timeout: kFastOperation)..stop();
|
||||||
|
logger.startProgress('BBB', timeout: kFastOperation)..stop();
|
||||||
expect(logger.statusText, 'AAA\nBBB\n');
|
expect(logger.statusText, 'AAA\nBBB\n');
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
Logger: () => BufferLogger(),
|
Logger: () => BufferLogger(),
|
||||||
|
@ -165,7 +165,7 @@ Stream<String> mockStdInStream;
|
|||||||
|
|
||||||
class TestTerminal extends AnsiTerminal {
|
class TestTerminal extends AnsiTerminal {
|
||||||
@override
|
@override
|
||||||
Stream<String> get onCharInput {
|
Stream<String> get keystrokes {
|
||||||
return mockStdInStream;
|
return mockStdInStream;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,15 @@ import 'dart:async';
|
|||||||
import 'package:file/memory.dart';
|
import 'package:file/memory.dart';
|
||||||
import 'package:flutter_tools/src/base/common.dart';
|
import 'package:flutter_tools/src/base/common.dart';
|
||||||
import 'package:flutter_tools/src/base/file_system.dart';
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
|
import 'package:flutter_tools/src/base/logger.dart';
|
||||||
import 'package:flutter_tools/src/base/platform.dart';
|
import 'package:flutter_tools/src/base/platform.dart';
|
||||||
|
import 'package:flutter_tools/src/base/terminal.dart';
|
||||||
import 'package:flutter_tools/src/cache.dart';
|
import 'package:flutter_tools/src/cache.dart';
|
||||||
import 'package:flutter_tools/src/commands/attach.dart';
|
import 'package:flutter_tools/src/commands/attach.dart';
|
||||||
import 'package:flutter_tools/src/device.dart';
|
import 'package:flutter_tools/src/device.dart';
|
||||||
import 'package:flutter_tools/src/resident_runner.dart';
|
import 'package:flutter_tools/src/resident_runner.dart';
|
||||||
import 'package:flutter_tools/src/run_hot.dart';
|
import 'package:flutter_tools/src/run_hot.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
|
|
||||||
import '../src/common.dart';
|
import '../src/common.dart';
|
||||||
@ -20,6 +23,7 @@ import '../src/context.dart';
|
|||||||
import '../src/mocks.dart';
|
import '../src/mocks.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
final StreamLogger logger = StreamLogger();
|
||||||
group('attach', () {
|
group('attach', () {
|
||||||
final FileSystem testFileSystem = MemoryFileSystem(
|
final FileSystem testFileSystem = MemoryFileSystem(
|
||||||
style: platform.isWindows ? FileSystemStyle.windows : FileSystemStyle
|
style: platform.isWindows ? FileSystemStyle.windows : FileSystemStyle
|
||||||
@ -48,18 +52,19 @@ void main() {
|
|||||||
// Now that the reader is used, start writing messages to it.
|
// Now that the reader is used, start writing messages to it.
|
||||||
Timer.run(() {
|
Timer.run(() {
|
||||||
mockLogReader.addLine('Foo');
|
mockLogReader.addLine('Foo');
|
||||||
mockLogReader.addLine(
|
mockLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort');
|
||||||
'Observatory listening on http://127.0.0.1:$devicePort');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return mockLogReader;
|
return mockLogReader;
|
||||||
});
|
});
|
||||||
when(device.portForwarder).thenReturn(portForwarder);
|
when(device.portForwarder)
|
||||||
|
.thenReturn(portForwarder);
|
||||||
when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
|
when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
|
||||||
.thenAnswer((_) async => hostPort);
|
.thenAnswer((_) async => hostPort);
|
||||||
when(portForwarder.forwardedPorts).thenReturn(
|
when(portForwarder.forwardedPorts)
|
||||||
<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
|
.thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
|
||||||
when(portForwarder.unforward(any)).thenAnswer((_) async => null);
|
when(portForwarder.unforward(any))
|
||||||
|
.thenAnswer((_) async => null);
|
||||||
|
|
||||||
// We cannot add the device to a device manager because that is
|
// We cannot add the device to a device manager because that is
|
||||||
// only enabled by the context of each testUsingContext call.
|
// only enabled by the context of each testUsingContext call.
|
||||||
@ -74,16 +79,23 @@ void main() {
|
|||||||
|
|
||||||
testUsingContext('finds observatory port and forwards', () async {
|
testUsingContext('finds observatory port and forwards', () async {
|
||||||
testDeviceManager.addDevice(device);
|
testDeviceManager.addDevice(device);
|
||||||
|
final Completer<void> completer = Completer<void>();
|
||||||
final AttachCommand command = AttachCommand();
|
final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
|
||||||
|
if (message == '[stdout] Done.') {
|
||||||
await createTestCommandRunner(command).run(<String>['attach']);
|
// The "Done." message is output by the AttachCommand when it's done.
|
||||||
|
completer.complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final Future<void> task = createTestCommandRunner(AttachCommand()).run(<String>['attach']);
|
||||||
|
await completer.future;
|
||||||
verify(
|
verify(
|
||||||
portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')),
|
portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')),
|
||||||
).called(1);
|
).called(1);
|
||||||
|
await expectLoggerInterruptEndsTask(task, logger);
|
||||||
|
await loggerSubscription.cancel();
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
FileSystem: () => testFileSystem,
|
FileSystem: () => testFileSystem,
|
||||||
|
Logger: () => logger,
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('accepts filesystem parameters', () async {
|
testUsingContext('accepts filesystem parameters', () async {
|
||||||
@ -94,6 +106,9 @@ void main() {
|
|||||||
const String projectRoot = '/build-output/project-root';
|
const String projectRoot = '/build-output/project-root';
|
||||||
const String outputDill = '/tmp/output.dill';
|
const String outputDill = '/tmp/output.dill';
|
||||||
|
|
||||||
|
final MockHotRunner mockHotRunner = MockHotRunner();
|
||||||
|
when(mockHotRunner.attach()).thenAnswer((_) async => 0);
|
||||||
|
|
||||||
final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory();
|
final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory();
|
||||||
when(
|
when(
|
||||||
mockHotRunnerFactory.build(
|
mockHotRunnerFactory.build(
|
||||||
@ -106,7 +121,7 @@ void main() {
|
|||||||
usesTerminalUI: anyNamed('usesTerminalUI'),
|
usesTerminalUI: anyNamed('usesTerminalUI'),
|
||||||
ipv6: false,
|
ipv6: false,
|
||||||
),
|
),
|
||||||
)..thenReturn(MockHotRunner());
|
).thenReturn(mockHotRunner);
|
||||||
|
|
||||||
final AttachCommand command = AttachCommand(
|
final AttachCommand command = AttachCommand(
|
||||||
hotRunnerFactory: mockHotRunnerFactory,
|
hotRunnerFactory: mockHotRunnerFactory,
|
||||||
@ -121,7 +136,7 @@ void main() {
|
|||||||
projectRoot,
|
projectRoot,
|
||||||
'--output-dill',
|
'--output-dill',
|
||||||
outputDill,
|
outputDill,
|
||||||
'-v',
|
'-v', // enables verbose logging
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Validate the attach call built a mock runner with the right
|
// Validate the attach call built a mock runner with the right
|
||||||
@ -191,30 +206,36 @@ void main() {
|
|||||||
final MockDeviceLogReader mockLogReader = MockDeviceLogReader();
|
final MockDeviceLogReader mockLogReader = MockDeviceLogReader();
|
||||||
final MockPortForwarder portForwarder = MockPortForwarder();
|
final MockPortForwarder portForwarder = MockPortForwarder();
|
||||||
final MockAndroidDevice device = MockAndroidDevice();
|
final MockAndroidDevice device = MockAndroidDevice();
|
||||||
|
final MockHotRunner mockHotRunner = MockHotRunner();
|
||||||
final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory();
|
final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory();
|
||||||
when(device.portForwarder).thenReturn(portForwarder);
|
when(device.portForwarder)
|
||||||
|
.thenReturn(portForwarder);
|
||||||
when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
|
when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
|
||||||
.thenAnswer((_) async => hostPort);
|
.thenAnswer((_) async => hostPort);
|
||||||
when(portForwarder.forwardedPorts).thenReturn(
|
when(portForwarder.forwardedPorts)
|
||||||
<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
|
.thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
|
||||||
when(portForwarder.unforward(any)).thenAnswer((_) async => null);
|
when(portForwarder.unforward(any))
|
||||||
when(mockHotRunnerFactory.build(any,
|
.thenAnswer((_) async => null);
|
||||||
|
when(mockHotRunner.attach())
|
||||||
|
.thenAnswer((_) async => 0);
|
||||||
|
when(mockHotRunnerFactory.build(
|
||||||
|
any,
|
||||||
target: anyNamed('target'),
|
target: anyNamed('target'),
|
||||||
debuggingOptions: anyNamed('debuggingOptions'),
|
debuggingOptions: anyNamed('debuggingOptions'),
|
||||||
packagesFilePath: anyNamed('packagesFilePath'),
|
packagesFilePath: anyNamed('packagesFilePath'),
|
||||||
usesTerminalUI: anyNamed('usesTerminalUI'),
|
usesTerminalUI: anyNamed('usesTerminalUI'),
|
||||||
ipv6: false)).thenReturn(
|
ipv6: false,
|
||||||
MockHotRunner());
|
)).thenReturn(mockHotRunner);
|
||||||
|
|
||||||
testDeviceManager.addDevice(device);
|
testDeviceManager.addDevice(device);
|
||||||
when(device.getLogReader()).thenAnswer((_) {
|
when(device.getLogReader())
|
||||||
|
.thenAnswer((_) {
|
||||||
// Now that the reader is used, start writing messages to it.
|
// Now that the reader is used, start writing messages to it.
|
||||||
Timer.run(() {
|
Timer.run(() {
|
||||||
mockLogReader.addLine('Foo');
|
mockLogReader.addLine('Foo');
|
||||||
mockLogReader.addLine(
|
mockLogReader.addLine(
|
||||||
'Observatory listening on http://127.0.0.1:$devicePort');
|
'Observatory listening on http://127.0.0.1:$devicePort');
|
||||||
});
|
});
|
||||||
|
|
||||||
return mockLogReader;
|
return mockLogReader;
|
||||||
});
|
});
|
||||||
final File foo = fs.file('lib/foo.dart')
|
final File foo = fs.file('lib/foo.dart')
|
||||||
@ -223,20 +244,20 @@ void main() {
|
|||||||
// Delete the main.dart file to be sure that attach works without it.
|
// Delete the main.dart file to be sure that attach works without it.
|
||||||
fs.file('lib/main.dart').deleteSync();
|
fs.file('lib/main.dart').deleteSync();
|
||||||
|
|
||||||
final AttachCommand command = AttachCommand(
|
final AttachCommand command = AttachCommand(hotRunnerFactory: mockHotRunnerFactory);
|
||||||
hotRunnerFactory: mockHotRunnerFactory);
|
await createTestCommandRunner(command).run(<String>['attach', '-t', foo.path, '-v']);
|
||||||
await createTestCommandRunner(command).run(
|
|
||||||
<String>['attach', '-t', foo.path, '-v']);
|
|
||||||
|
|
||||||
verify(mockHotRunnerFactory.build(any,
|
verify(mockHotRunnerFactory.build(
|
||||||
|
any,
|
||||||
target: foo.path,
|
target: foo.path,
|
||||||
debuggingOptions: anyNamed('debuggingOptions'),
|
debuggingOptions: anyNamed('debuggingOptions'),
|
||||||
packagesFilePath: anyNamed('packagesFilePath'),
|
packagesFilePath: anyNamed('packagesFilePath'),
|
||||||
usesTerminalUI: anyNamed('usesTerminalUI'),
|
usesTerminalUI: anyNamed('usesTerminalUI'),
|
||||||
ipv6: false)).called(1);
|
ipv6: false,
|
||||||
|
)).called(1);
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
FileSystem: () => testFileSystem,
|
FileSystem: () => testFileSystem,
|
||||||
},);
|
});
|
||||||
|
|
||||||
group('forwarding to given port', () {
|
group('forwarding to given port', () {
|
||||||
const int devicePort = 499;
|
const int devicePort = 499;
|
||||||
@ -248,42 +269,74 @@ void main() {
|
|||||||
portForwarder = MockPortForwarder();
|
portForwarder = MockPortForwarder();
|
||||||
device = MockAndroidDevice();
|
device = MockAndroidDevice();
|
||||||
|
|
||||||
when(device.portForwarder).thenReturn(portForwarder);
|
when(device.portForwarder)
|
||||||
when(portForwarder.forward(devicePort)).thenAnswer((_) async => hostPort);
|
.thenReturn(portForwarder);
|
||||||
when(portForwarder.forwardedPorts).thenReturn(
|
when(portForwarder.forward(devicePort))
|
||||||
<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
|
.thenAnswer((_) async => hostPort);
|
||||||
when(portForwarder.unforward(any)).thenAnswer((_) async => null);
|
when(portForwarder.forwardedPorts)
|
||||||
|
.thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
|
||||||
|
when(portForwarder.unforward(any))
|
||||||
|
.thenAnswer((_) async => null);
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('succeeds in ipv4 mode', () async {
|
testUsingContext('succeeds in ipv4 mode', () async {
|
||||||
testDeviceManager.addDevice(device);
|
testDeviceManager.addDevice(device);
|
||||||
final AttachCommand command = AttachCommand();
|
|
||||||
|
|
||||||
await createTestCommandRunner(command).run(
|
|
||||||
<String>['attach', '--debug-port', '$devicePort']);
|
|
||||||
|
|
||||||
|
final Completer<void> completer = Completer<void>();
|
||||||
|
final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
|
||||||
|
if (message == '[verbose] Connecting to service protocol: http://127.0.0.1:42/') {
|
||||||
|
// Wait until resident_runner.dart tries to connect.
|
||||||
|
// There's nothing to connect _to_, so that's as far as we care to go.
|
||||||
|
completer.complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final Future<void> task = createTestCommandRunner(AttachCommand())
|
||||||
|
.run(<String>['attach', '--debug-port', '$devicePort']);
|
||||||
|
await completer.future;
|
||||||
verify(portForwarder.forward(devicePort)).called(1);
|
verify(portForwarder.forward(devicePort)).called(1);
|
||||||
|
|
||||||
|
await expectLoggerInterruptEndsTask(task, logger);
|
||||||
|
await loggerSubscription.cancel();
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
FileSystem: () => testFileSystem,
|
FileSystem: () => testFileSystem,
|
||||||
|
Logger: () => logger,
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('succeeds in ipv6 mode', () async {
|
testUsingContext('succeeds in ipv6 mode', () async {
|
||||||
testDeviceManager.addDevice(device);
|
testDeviceManager.addDevice(device);
|
||||||
final AttachCommand command = AttachCommand();
|
|
||||||
|
|
||||||
await createTestCommandRunner(command).run(
|
|
||||||
<String>['attach', '--debug-port', '$devicePort', '--ipv6']);
|
|
||||||
|
|
||||||
|
final Completer<void> completer = Completer<void>();
|
||||||
|
final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
|
||||||
|
if (message == '[verbose] Connecting to service protocol: http://[::1]:42/') {
|
||||||
|
// Wait until resident_runner.dart tries to connect.
|
||||||
|
// There's nothing to connect _to_, so that's as far as we care to go.
|
||||||
|
completer.complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final Future<void> task = createTestCommandRunner(AttachCommand())
|
||||||
|
.run(<String>['attach', '--debug-port', '$devicePort', '--ipv6']);
|
||||||
|
await completer.future;
|
||||||
verify(portForwarder.forward(devicePort)).called(1);
|
verify(portForwarder.forward(devicePort)).called(1);
|
||||||
|
|
||||||
|
await expectLoggerInterruptEndsTask(task, logger);
|
||||||
|
await loggerSubscription.cancel();
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
FileSystem: () => testFileSystem,
|
FileSystem: () => testFileSystem,
|
||||||
|
Logger: () => logger,
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('skips in ipv4 mode with a provided observatory port', () async {
|
testUsingContext('skips in ipv4 mode with a provided observatory port', () async {
|
||||||
testDeviceManager.addDevice(device);
|
testDeviceManager.addDevice(device);
|
||||||
final AttachCommand command = AttachCommand();
|
|
||||||
|
|
||||||
await createTestCommandRunner(command).run(
|
final Completer<void> completer = Completer<void>();
|
||||||
|
final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
|
||||||
|
if (message == '[verbose] Connecting to service protocol: http://127.0.0.1:42/') {
|
||||||
|
// Wait until resident_runner.dart tries to connect.
|
||||||
|
// There's nothing to connect _to_, so that's as far as we care to go.
|
||||||
|
completer.complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final Future<void> task = createTestCommandRunner(AttachCommand()).run(
|
||||||
<String>[
|
<String>[
|
||||||
'attach',
|
'attach',
|
||||||
'--debug-port',
|
'--debug-port',
|
||||||
@ -292,17 +345,28 @@ void main() {
|
|||||||
'$hostPort',
|
'$hostPort',
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
await completer.future;
|
||||||
verifyNever(portForwarder.forward(devicePort));
|
verifyNever(portForwarder.forward(devicePort));
|
||||||
|
|
||||||
|
await expectLoggerInterruptEndsTask(task, logger);
|
||||||
|
await loggerSubscription.cancel();
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
FileSystem: () => testFileSystem,
|
FileSystem: () => testFileSystem,
|
||||||
|
Logger: () => logger,
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('skips in ipv6 mode with a provided observatory port', () async {
|
testUsingContext('skips in ipv6 mode with a provided observatory port', () async {
|
||||||
testDeviceManager.addDevice(device);
|
testDeviceManager.addDevice(device);
|
||||||
final AttachCommand command = AttachCommand();
|
|
||||||
|
|
||||||
await createTestCommandRunner(command).run(
|
final Completer<void> completer = Completer<void>();
|
||||||
|
final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
|
||||||
|
if (message == '[verbose] Connecting to service protocol: http://[::1]:42/') {
|
||||||
|
// Wait until resident_runner.dart tries to connect.
|
||||||
|
// There's nothing to connect _to_, so that's as far as we care to go.
|
||||||
|
completer.complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final Future<void> task = createTestCommandRunner(AttachCommand()).run(
|
||||||
<String>[
|
<String>[
|
||||||
'attach',
|
'attach',
|
||||||
'--debug-port',
|
'--debug-port',
|
||||||
@ -312,10 +376,14 @@ void main() {
|
|||||||
'--ipv6',
|
'--ipv6',
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
await completer.future;
|
||||||
verifyNever(portForwarder.forward(devicePort));
|
verifyNever(portForwarder.forward(devicePort));
|
||||||
|
|
||||||
|
await expectLoggerInterruptEndsTask(task, logger);
|
||||||
|
await loggerSubscription.cancel();
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
FileSystem: () => testFileSystem,
|
FileSystem: () => testFileSystem,
|
||||||
|
Logger: () => logger,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -328,7 +396,7 @@ void main() {
|
|||||||
expect(testLogger.statusText, contains('No connected devices'));
|
expect(testLogger.statusText, contains('No connected devices'));
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
FileSystem: () => testFileSystem,
|
FileSystem: () => testFileSystem,
|
||||||
},);
|
});
|
||||||
|
|
||||||
testUsingContext('exits when multiple devices connected', () async {
|
testUsingContext('exits when multiple devices connected', () async {
|
||||||
Device aDeviceWithId(String id) {
|
Device aDeviceWithId(String id) {
|
||||||
@ -352,7 +420,7 @@ void main() {
|
|||||||
expect(testLogger.statusText, contains('yy2'));
|
expect(testLogger.statusText, contains('yy2'));
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
FileSystem: () => testFileSystem,
|
FileSystem: () => testFileSystem,
|
||||||
},);
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -361,3 +429,83 @@ class MockPortForwarder extends Mock implements DevicePortForwarder {}
|
|||||||
class MockHotRunner extends Mock implements HotRunner {}
|
class MockHotRunner extends Mock implements HotRunner {}
|
||||||
|
|
||||||
class MockHotRunnerFactory extends Mock implements HotRunnerFactory {}
|
class MockHotRunnerFactory extends Mock implements HotRunnerFactory {}
|
||||||
|
|
||||||
|
class StreamLogger extends Logger {
|
||||||
|
@override
|
||||||
|
bool get isVerbose => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void printError(
|
||||||
|
String message, {
|
||||||
|
StackTrace stackTrace,
|
||||||
|
bool emphasis,
|
||||||
|
TerminalColor color,
|
||||||
|
int indent,
|
||||||
|
int hangingIndent,
|
||||||
|
bool wrap,
|
||||||
|
}) {
|
||||||
|
_log('[stderr] $message');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void printStatus(
|
||||||
|
String message, {
|
||||||
|
bool emphasis,
|
||||||
|
TerminalColor color,
|
||||||
|
bool newline,
|
||||||
|
int indent,
|
||||||
|
int hangingIndent,
|
||||||
|
bool wrap,
|
||||||
|
}) {
|
||||||
|
_log('[stdout] $message');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void printTrace(String message) {
|
||||||
|
_log('[verbose] $message');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Status startProgress(
|
||||||
|
String message, {
|
||||||
|
@required Duration timeout,
|
||||||
|
String progressId,
|
||||||
|
bool multilineOutput = false,
|
||||||
|
int progressIndicatorPadding = kDefaultStatusPadding,
|
||||||
|
}) {
|
||||||
|
_log('[progress] $message');
|
||||||
|
return SilentStatus(timeout: timeout)..start();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _interrupt = false;
|
||||||
|
|
||||||
|
void interrupt() {
|
||||||
|
_interrupt = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final StreamController<String> _controller = StreamController<String>.broadcast();
|
||||||
|
|
||||||
|
void _log(String message) {
|
||||||
|
_controller.add(message);
|
||||||
|
if (_interrupt) {
|
||||||
|
_interrupt = false;
|
||||||
|
throw const LoggerInterrupted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<String> get stream => _controller.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoggerInterrupted implements Exception {
|
||||||
|
const LoggerInterrupted();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> expectLoggerInterruptEndsTask(Future<void> task, StreamLogger logger) async {
|
||||||
|
logger.interrupt(); // an exception during the task should cause it to fail...
|
||||||
|
try {
|
||||||
|
await task;
|
||||||
|
expect(false, isTrue); // (shouldn't reach here)
|
||||||
|
} on ToolExit catch (error) {
|
||||||
|
expect(error.exitCode, 2); // ...with exit code 2.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -90,5 +90,5 @@ void main() {
|
|||||||
expect(result, isList);
|
expect(result, isList);
|
||||||
expect(result, isNotEmpty);
|
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 {
|
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.
|
expect((await _flutter.getSourceLocation()).line, equals(_project.breakpointLine));
|
||||||
await _flutter.breakAt(_project.breakpointUri, _project.breakpointLine, restart: true);
|
|
||||||
|
|
||||||
// Issue 5 steps, ensuring that we end up on the annotated lines each time.
|
// 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();
|
await _flutter.stepOverOrOverAsyncSuspension();
|
||||||
final SourcePosition location = await _flutter.getSourceLocation();
|
final SourcePosition location = await _flutter.getSourceLocation();
|
||||||
final int actualLine = location.line;
|
final int actualLine = location.line;
|
||||||
@ -47,5 +49,5 @@ void main() {
|
|||||||
reason: 'After $i steps, debugger should stop at $expectedLine but stopped at $actualLine');
|
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.
|
||||||
}
|
}
|
||||||
|
@ -32,16 +32,18 @@ void main() {
|
|||||||
tryToDelete(tempDir);
|
tryToDelete(tempDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<Isolate> breakInBuildMethod(FlutterTestDriver flutter) async {
|
Future<void> breakInBuildMethod(FlutterTestDriver flutter) async {
|
||||||
return _flutter.breakAt(
|
await _flutter.breakAt(
|
||||||
_project.buildMethodBreakpointUri,
|
_project.buildMethodBreakpointUri,
|
||||||
_project.buildMethodBreakpointLine);
|
_project.buildMethodBreakpointLine,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Isolate> breakInTopLevelFunction(FlutterTestDriver flutter) async {
|
Future<void> breakInTopLevelFunction(FlutterTestDriver flutter) async {
|
||||||
return _flutter.breakAt(
|
await _flutter.breakAt(
|
||||||
_project.topLevelFunctionBreakpointUri,
|
_project.topLevelFunctionBreakpointUri,
|
||||||
_project.topLevelFunctionBreakpointLine);
|
_project.topLevelFunctionBreakpointLine,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
test('can evaluate trivial expressions in top level function', () async {
|
test('can evaluate trivial expressions in top level function', () async {
|
||||||
@ -79,7 +81,7 @@ void main() {
|
|||||||
await breakInBuildMethod(_flutter);
|
await breakInBuildMethod(_flutter);
|
||||||
await evaluateComplexReturningExpressions(_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.
|
||||||
|
|
||||||
group('flutter test expression evaluation', () {
|
group('flutter test expression evaluation', () {
|
||||||
Directory tempDir;
|
Directory tempDir;
|
||||||
@ -124,7 +126,7 @@ void main() {
|
|||||||
await evaluateComplexReturningExpressions(_flutter);
|
await evaluateComplexReturningExpressions(_flutter);
|
||||||
});
|
});
|
||||||
// Skipped due to https://github.com/flutter/flutter/issues/26518
|
// Skipped due to https://github.com/flutter/flutter/issues/26518
|
||||||
}, timeout: const Timeout.factor(6));
|
}, timeout: const Timeout.factor(10), skip: true); // The DevFS sync takes a really long time, so these tests can be slow.
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> evaluateTrivialExpressions(FlutterTestDriver flutter) async {
|
Future<void> evaluateTrivialExpressions(FlutterTestDriver flutter) async {
|
||||||
|
@ -18,8 +18,8 @@ void main() {
|
|||||||
setUp(() async {
|
setUp(() async {
|
||||||
tempDir = createResolvedTempDirectorySync('attach_test.');
|
tempDir = createResolvedTempDirectorySync('attach_test.');
|
||||||
await _project.setUpIn(tempDir);
|
await _project.setUpIn(tempDir);
|
||||||
_flutterRun = FlutterRunTestDriver(tempDir, logPrefix: 'RUN');
|
_flutterRun = FlutterRunTestDriver(tempDir, logPrefix: ' RUN ');
|
||||||
_flutterAttach = FlutterRunTestDriver(tempDir, logPrefix: 'ATTACH');
|
_flutterAttach = FlutterRunTestDriver(tempDir, logPrefix: 'ATTACH ');
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
@ -58,5 +58,5 @@ void main() {
|
|||||||
await _flutterAttach.attach(_flutterRun.vmServicePort);
|
await _flutterAttach.attach(_flutterRun.vmServicePort);
|
||||||
await _flutterAttach.hotReload();
|
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);
|
await _flutter.run(pidFile: pidFile);
|
||||||
expect(pidFile.existsSync(), isTrue);
|
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';
|
import 'test_utils.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('hot', () {
|
group('hot reload tests', () {
|
||||||
Directory tempDir;
|
Directory tempDir;
|
||||||
final HotReloadProject _project = HotReloadProject();
|
final HotReloadProject _project = HotReloadProject();
|
||||||
FlutterRunTestDriver _flutter;
|
FlutterRunTestDriver _flutter;
|
||||||
@ -26,39 +26,127 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
await _flutter.stop();
|
await _flutter?.stop();
|
||||||
tryToDelete(tempDir);
|
tryToDelete(tempDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reload works without error', () async {
|
test('hot reload works without error', () async {
|
||||||
await _flutter.run();
|
await _flutter.run();
|
||||||
await _flutter.hotReload();
|
await _flutter.hotReload();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('newly added code executes during reload', () async {
|
test('newly added code executes during hot reload', () async {
|
||||||
await _flutter.run();
|
await _flutter.run();
|
||||||
_project.uncommentHotReloadPrint();
|
_project.uncommentHotReloadPrint();
|
||||||
final StringBuffer stdout = StringBuffer();
|
final StringBuffer stdout = StringBuffer();
|
||||||
final StreamSubscription<String> sub = _flutter.stdout.listen(stdout.writeln);
|
final StreamSubscription<String> subscription = _flutter.stdout.listen(stdout.writeln);
|
||||||
try {
|
try {
|
||||||
await _flutter.hotReload();
|
await _flutter.hotReload();
|
||||||
expect(stdout.toString(), contains('(((((RELOAD WORKED)))))'));
|
expect(stdout.toString(), contains('(((((RELOAD WORKED)))))'));
|
||||||
} finally {
|
} 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.run();
|
||||||
await _flutter.hotRestart();
|
await _flutter.hotRestart();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reload hits breakpoints after reload', () async {
|
test('breakpoints are hit after hot reload', () async {
|
||||||
await _flutter.run(withDebugger: true);
|
Isolate isolate;
|
||||||
final Isolate isolate = await _flutter.breakAt(
|
await _flutter.run(withDebugger: true, startPaused: true);
|
||||||
_project.breakpointUri,
|
final Completer<void> sawTick1 = Completer<void>();
|
||||||
_project.breakpointLine);
|
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));
|
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();
|
||||||
});
|
});
|
||||||
}, timeout: const Timeout.factor(6));
|
|
||||||
|
test('hot reload doesn\'t reassemble if paused', () async {
|
||||||
|
await _flutter.run(withDebugger: true);
|
||||||
|
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(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);
|
await Future<void>.delayed(requiredLifespan);
|
||||||
expect(_flutter.hasExited, equals(false));
|
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
|
@override
|
||||||
final String main = r'''
|
final String main = r'''
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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 {
|
class MyApp extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
topLevelFunction();
|
topLevelFunction();
|
||||||
return new MaterialApp( // BREAKPOINT
|
return new MaterialApp( // BUILD BREAKPOINT
|
||||||
title: 'Flutter Demo',
|
title: 'Flutter Demo',
|
||||||
home: new Container(),
|
home: new Container(),
|
||||||
);
|
);
|
||||||
@ -39,9 +46,9 @@ class BasicProject extends Project {
|
|||||||
}
|
}
|
||||||
''';
|
''';
|
||||||
|
|
||||||
Uri get buildMethodBreakpointUri => breakpointUri;
|
Uri get buildMethodBreakpointUri => mainDart;
|
||||||
int get buildMethodBreakpointLine => breakpointLine;
|
int get buildMethodBreakpointLine => lineContaining(main, '// BUILD BREAKPOINT');
|
||||||
|
|
||||||
Uri get topLevelFunctionBreakpointUri => breakpointUri;
|
Uri get topLevelFunctionBreakpointUri => mainDart;
|
||||||
int get topLevelFunctionBreakpointLine => lineContaining(main, '// TOP LEVEL BREAKPOINT');
|
int get topLevelFunctionBreakpointLine => lineContaining(main, '// TOP LEVEL BREAKPOINT');
|
||||||
}
|
}
|
||||||
|
@ -22,33 +22,63 @@ class HotReloadProject extends Project {
|
|||||||
@override
|
@override
|
||||||
final String main = r'''
|
final String main = r'''
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
void main() => runApp(new MyApp());
|
void main() => runApp(new MyApp());
|
||||||
|
|
||||||
|
int count = 1;
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Do not remove this line, it's uncommented by a test to verify that hot
|
// This method gets called each time we hot reload, during reassemble.
|
||||||
// reloading worked.
|
|
||||||
|
// Do not remove the next line, it's uncommented by a test to verify that
|
||||||
|
// hot reloading worked:
|
||||||
// printHotReloadWorked();
|
// 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',
|
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
|
// The call to this function is uncommented by a test to verify that hot
|
||||||
// reloading worked.
|
// reloading worked.
|
||||||
print('(((((RELOAD 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() {
|
void uncommentHotReloadPrint() {
|
||||||
final String newMainContents = main.replaceAll(
|
final String newMainContents = main.replaceAll(
|
||||||
'// printHotReloadWorked();', 'printHotReloadWorked();');
|
'// printHotReloadWorked();',
|
||||||
|
'printHotReloadWorked();'
|
||||||
|
);
|
||||||
writeFile(fs.path.join(dir.path, 'lib', 'main.dart'), newMainContents);
|
writeFile(fs.path.join(dir.path, 'lib', 'main.dart'), newMainContents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,7 @@ abstract class Project {
|
|||||||
String get pubspec;
|
String get pubspec;
|
||||||
String get main;
|
String get main;
|
||||||
|
|
||||||
// Valid locations for a breakpoint for tests that just need to break somewhere.
|
Uri get mainDart => Uri.parse('package:test/main.dart');
|
||||||
Uri get breakpointUri => Uri.parse('package:test/main.dart');
|
|
||||||
int get breakpointLine => lineContaining(main, '// BREAKPOINT');
|
|
||||||
|
|
||||||
Future<void> setUpIn(Directory dir) async {
|
Future<void> setUpIn(Directory dir) async {
|
||||||
this.dir = dir;
|
this.dir = dir;
|
||||||
@ -32,6 +30,6 @@ abstract class Project {
|
|||||||
final int index = contents.split('\n').indexWhere((String l) => l.contains(search));
|
final int index = contents.split('\n').indexWhere((String l) => l.contains(search));
|
||||||
if (index == -1)
|
if (index == -1)
|
||||||
throw Exception("Did not find '$search' inside the file");
|
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 {
|
Future<void> doAsyncStuff() async {
|
||||||
print("test"); // BREAKPOINT
|
print("test"); // BREAKPOINT
|
||||||
await new Future.value(true); // STEP 1
|
await new Future.value(true); // STEP 1 // STEP 2
|
||||||
await new Future.microtask(() => true); // STEP 2 // STEP 3
|
await new Future.microtask(() => true); // STEP 3 // STEP 4
|
||||||
await new Future.delayed(const Duration(milliseconds: 1)); // STEP 4 // STEP 5
|
await new Future.delayed(const Duration(milliseconds: 1)); // STEP 5 // STEP 6
|
||||||
print("done!"); // STEP 6
|
print("done!"); // STEP 7
|
||||||
}
|
} // STEP 8
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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');
|
int lineForStep(int i) => lineContaining(main, '// STEP $i');
|
||||||
|
|
||||||
final int numberOfSteps = 6;
|
final int numberOfSteps = 8;
|
||||||
}
|
}
|
||||||
|
@ -47,9 +47,7 @@ class TestsProject extends Project {
|
|||||||
|
|
||||||
String get testFilePath => fs.path.join(dir.path, 'test', 'test.dart');
|
String get testFilePath => fs.path.join(dir.path, 'test', 'test.dart');
|
||||||
|
|
||||||
@override
|
|
||||||
Uri get breakpointUri => Uri.file(testFilePath);
|
Uri get breakpointUri => Uri.file(testFilePath);
|
||||||
|
|
||||||
@override
|
|
||||||
int get breakpointLine => lineContaining(testContent, '// BREAKPOINT');
|
int get breakpointLine => lineContaining(testContent, '// BREAKPOINT');
|
||||||
}
|
}
|
||||||
|
@ -8,26 +8,41 @@ import 'dart:convert';
|
|||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:flutter_tools/src/base/file_system.dart';
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
import 'package:flutter_tools/src/base/io.dart';
|
import 'package:flutter_tools/src/base/io.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
import 'package:process/process.dart';
|
import 'package:process/process.dart';
|
||||||
import 'package:vm_service_lib/vm_service_lib.dart';
|
import 'package:vm_service_lib/vm_service_lib.dart';
|
||||||
import 'package:vm_service_lib/vm_service_lib_io.dart';
|
import 'package:vm_service_lib/vm_service_lib_io.dart';
|
||||||
|
|
||||||
import '../src/common.dart';
|
import '../src/common.dart';
|
||||||
|
|
||||||
// Set this to true for debugging to get JSON written to stdout.
|
// Set this to true for debugging to get verbose logs written to stdout.
|
||||||
|
// The logs include the following:
|
||||||
|
// <=stdout= data that the flutter tool running in --verbose mode wrote to stdout.
|
||||||
|
// <=stderr= data that the flutter tool running in --verbose mode wrote to stderr.
|
||||||
|
// =stdin=> data that the test sent to the flutter tool over stdin.
|
||||||
|
// =vm=> data that was sent over the VM service channel to the app running on the test device.
|
||||||
|
// <=vm= data that was sent from the app on the test device over the VM service channel.
|
||||||
|
// Messages regarding what the test is doing.
|
||||||
|
// If this is false, then only critical errors and logs when things appear to be
|
||||||
|
// taking a long time are printed to the console.
|
||||||
const bool _printDebugOutputToStdOut = false;
|
const bool _printDebugOutputToStdOut = false;
|
||||||
const Duration defaultTimeout = Duration(seconds: 40);
|
|
||||||
|
final DateTime startTime = DateTime.now();
|
||||||
|
|
||||||
|
const Duration defaultTimeout = Duration(seconds: 5);
|
||||||
const Duration appStartTimeout = Duration(seconds: 120);
|
const Duration appStartTimeout = Duration(seconds: 120);
|
||||||
const Duration quitTimeout = Duration(seconds: 10);
|
const Duration quitTimeout = Duration(seconds: 10);
|
||||||
|
|
||||||
abstract class FlutterTestDriver {
|
abstract class FlutterTestDriver {
|
||||||
FlutterTestDriver(this._projectFolder, {String logPrefix}):
|
FlutterTestDriver(
|
||||||
_logPrefix = logPrefix != null ? '$logPrefix: ' : '';
|
this._projectFolder, {
|
||||||
|
String logPrefix,
|
||||||
|
}) : _logPrefix = logPrefix != null ? '$logPrefix: ' : '';
|
||||||
|
|
||||||
final Directory _projectFolder;
|
final Directory _projectFolder;
|
||||||
final String _logPrefix;
|
final String _logPrefix;
|
||||||
Process _proc;
|
Process _process;
|
||||||
int _procPid;
|
int _processPid;
|
||||||
final StreamController<String> _stdout = StreamController<String>.broadcast();
|
final StreamController<String> _stdout = StreamController<String>.broadcast();
|
||||||
final StreamController<String> _stderr = StreamController<String>.broadcast();
|
final StreamController<String> _stderr = StreamController<String>.broadcast();
|
||||||
final StreamController<String> _allMessages = StreamController<String>.broadcast();
|
final StreamController<String> _allMessages = StreamController<String>.broadcast();
|
||||||
@ -42,101 +57,108 @@ abstract class FlutterTestDriver {
|
|||||||
int get vmServicePort => _vmServiceWsUri.port;
|
int get vmServicePort => _vmServiceWsUri.port;
|
||||||
bool get hasExited => _hasExited;
|
bool get hasExited => _hasExited;
|
||||||
|
|
||||||
String _debugPrint(String msg) {
|
String lastTime = '';
|
||||||
const int maxLength = 500;
|
void _debugPrint(String message, { String topic = '' }) {
|
||||||
final String truncatedMsg =
|
const int maxLength = 2500;
|
||||||
msg.length > maxLength ? msg.substring(0, maxLength) + '...' : msg;
|
final String truncatedMessage = message.length > maxLength ? message.substring(0, maxLength) + '...' : message;
|
||||||
_allMessages.add(truncatedMsg);
|
final String line = '${topic.padRight(10)} $truncatedMessage';
|
||||||
if (_printDebugOutputToStdOut) {
|
_allMessages.add(line);
|
||||||
print('$_logPrefix$truncatedMsg');
|
final int timeInSeconds = DateTime.now().difference(startTime).inSeconds;
|
||||||
|
String time = timeInSeconds.toString().padLeft(5) + 's ';
|
||||||
|
if (time == lastTime) {
|
||||||
|
time = ' ' * time.length;
|
||||||
|
} else {
|
||||||
|
lastTime = time;
|
||||||
}
|
}
|
||||||
return msg;
|
if (_printDebugOutputToStdOut)
|
||||||
|
print('$time$_logPrefix$line');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _setupProcess(
|
Future<void> _setupProcess(
|
||||||
List<String> args, {
|
List<String> arguments, {
|
||||||
String script,
|
String script,
|
||||||
bool withDebugger = false,
|
bool withDebugger = false,
|
||||||
bool pauseOnExceptions = false,
|
|
||||||
File pidFile,
|
File pidFile,
|
||||||
}) async {
|
}) async {
|
||||||
final String flutterBin = fs.path.join(getFlutterRoot(), 'bin', 'flutter');
|
final String flutterBin = fs.path.join(getFlutterRoot(), 'bin', 'flutter');
|
||||||
if (withDebugger) {
|
if (withDebugger)
|
||||||
args.add('--start-paused');
|
arguments.add('--start-paused');
|
||||||
}
|
if (_printDebugOutputToStdOut)
|
||||||
|
arguments.add('--verbose');
|
||||||
if (pidFile != null) {
|
if (pidFile != null) {
|
||||||
args.addAll(<String>['--pid-file', pidFile.path]);
|
arguments.addAll(<String>['--pid-file', pidFile.path]);
|
||||||
}
|
}
|
||||||
if (script != null) {
|
if (script != null) {
|
||||||
args.add(script);
|
arguments.add(script);
|
||||||
}
|
}
|
||||||
_debugPrint('Spawning flutter $args in ${_projectFolder.path}');
|
_debugPrint('Spawning flutter $arguments in ${_projectFolder.path}');
|
||||||
|
|
||||||
const ProcessManager _processManager = LocalProcessManager();
|
const ProcessManager _processManager = LocalProcessManager();
|
||||||
_proc = await _processManager.start(
|
_process = await _processManager.start(
|
||||||
<String>[flutterBin]
|
<String>[flutterBin]
|
||||||
.followedBy(args)
|
.followedBy(arguments)
|
||||||
.toList(),
|
.toList(),
|
||||||
workingDirectory: _projectFolder.path,
|
workingDirectory: _projectFolder.path,
|
||||||
environment: <String, String>{'FLUTTER_TEST': 'true'});
|
environment: <String, String>{'FLUTTER_TEST': 'true'},
|
||||||
|
);
|
||||||
|
|
||||||
// This class doesn't use the result of the future. It's made available
|
// This class doesn't use the result of the future. It's made available
|
||||||
// via a getter for external uses.
|
// via a getter for external uses.
|
||||||
_proc.exitCode.then((int code) { // ignore: unawaited_futures
|
_process.exitCode.then((int code) { // ignore: unawaited_futures
|
||||||
_debugPrint('Process exited ($code)');
|
_debugPrint('Process exited ($code)');
|
||||||
_hasExited = true;
|
_hasExited = true;
|
||||||
});
|
});
|
||||||
transformToLines(_proc.stdout).listen((String line) => _stdout.add(line));
|
transformToLines(_process.stdout).listen((String line) => _stdout.add(line));
|
||||||
transformToLines(_proc.stderr).listen((String line) => _stderr.add(line));
|
transformToLines(_process.stderr).listen((String line) => _stderr.add(line));
|
||||||
|
|
||||||
// Capture stderr to a buffer so we can show it all if any requests fail.
|
// Capture stderr to a buffer so we can show it all if any requests fail.
|
||||||
_stderr.stream.listen(_errorBuffer.writeln);
|
_stderr.stream.listen(_errorBuffer.writeln);
|
||||||
|
|
||||||
// This is just debug printing to aid running/debugging tests locally.
|
// This is just debug printing to aid running/debugging tests locally.
|
||||||
_stdout.stream.listen(_debugPrint);
|
_stdout.stream.listen((String message) => _debugPrint(message, topic: '<=stdout='));
|
||||||
_stderr.stream.listen(_debugPrint);
|
_stderr.stream.listen((String message) => _debugPrint(message, topic: '<=stderr='));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> connectToVmService({bool pauseOnExceptions = false}) async {
|
Future<void> connectToVmService({bool pauseOnExceptions = false}) async {
|
||||||
_vmService = await vmServiceConnectUri(_vmServiceWsUri.toString());
|
_vmService = await vmServiceConnectUri('$_vmServiceWsUri');
|
||||||
_vmService.onSend.listen((String s) => _debugPrint('==> $s'));
|
_vmService.onSend.listen((String s) => _debugPrint(s, topic: '=vm=>'));
|
||||||
_vmService.onReceive.listen((String s) => _debugPrint('<== $s'));
|
_vmService.onReceive.listen((String s) => _debugPrint(s, topic: '<=vm='));
|
||||||
|
_vmService.onIsolateEvent.listen((Event event) {
|
||||||
|
if (event.kind == EventKind.kIsolateExit && event.isolate.id == _flutterIsolateId) {
|
||||||
|
// Hot restarts cause all the isolates to exit, so we need to refresh
|
||||||
|
// our idea of what the Flutter isolate ID is.
|
||||||
|
_flutterIsolateId = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await Future.wait(<Future<Success>>[
|
await Future.wait(<Future<Success>>[
|
||||||
_vmService.streamListen('Isolate'),
|
_vmService.streamListen('Isolate'),
|
||||||
_vmService.streamListen('Debug'),
|
_vmService.streamListen('Debug'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// On hot restarts, the isolate ID we have for the Flutter thread will
|
|
||||||
// exit so we need to invalidate our cached ID.
|
|
||||||
_vmService.onIsolateEvent.listen((Event event) {
|
|
||||||
if (event.kind == EventKind.kIsolateExit && event.isolate.id == _flutterIsolateId) {
|
|
||||||
_flutterIsolateId = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Because we start paused, resume so the app is in a "running" state as
|
|
||||||
// expected by tests. Tests will reload/restart as required if they need
|
|
||||||
// to hit breakpoints, etc.
|
|
||||||
await waitForPause();
|
await waitForPause();
|
||||||
if (pauseOnExceptions) {
|
if (pauseOnExceptions) {
|
||||||
await _vmService.setExceptionPauseMode(await _getFlutterIsolateId(), ExceptionPauseMode.kUnhandled);
|
await _vmService.setExceptionPauseMode(
|
||||||
|
await _getFlutterIsolateId(),
|
||||||
|
ExceptionPauseMode.kUnhandled,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> quit() => _killGracefully();
|
Future<int> quit() => _killGracefully();
|
||||||
|
|
||||||
Future<int> _killGracefully() async {
|
Future<int> _killGracefully() async {
|
||||||
if (_procPid == null)
|
if (_processPid == null)
|
||||||
return -1;
|
return -1;
|
||||||
_debugPrint('Sending SIGTERM to $_procPid..');
|
_debugPrint('Sending SIGTERM to $_processPid..');
|
||||||
Process.killPid(_procPid);
|
Process.killPid(_processPid);
|
||||||
return _proc.exitCode.timeout(quitTimeout, onTimeout: _killForcefully);
|
return _process.exitCode.timeout(quitTimeout, onTimeout: _killForcefully);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> _killForcefully() {
|
Future<int> _killForcefully() {
|
||||||
_debugPrint('Sending SIGKILL to $_procPid..');
|
_debugPrint('Sending SIGKILL to $_processPid..');
|
||||||
Process.killPid(_procPid, ProcessSignal.SIGKILL);
|
Process.killPid(_processPid, ProcessSignal.SIGKILL);
|
||||||
return _proc.exitCode;
|
return _process.exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _flutterIsolateId;
|
String _flutterIsolateId;
|
||||||
@ -155,75 +177,106 @@ abstract class FlutterTestDriver {
|
|||||||
return isolate;
|
return isolate;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addBreakpoint(Uri uri, int line) async {
|
/// Add a breakpoint and wait for it to trip the program execution.
|
||||||
_debugPrint('Sending breakpoint for $uri:$line');
|
///
|
||||||
await _vmService.addBreakpointWithScriptUri(
|
/// Only call this when you are absolutely sure that the program under test
|
||||||
await _getFlutterIsolateId(), uri.toString(), line);
|
/// will hit the breakpoint _in the future_.
|
||||||
|
///
|
||||||
|
/// In particular, do not call this if the program is currently racing to pass
|
||||||
|
/// the line of code you are breaking on. Pretend that calling this will take
|
||||||
|
/// an hour before setting the breakpoint. Would the code still eventually hit
|
||||||
|
/// the breakpoint and stop?
|
||||||
|
Future<void> breakAt(Uri uri, int line) async {
|
||||||
|
await addBreakpoint(uri, line);
|
||||||
|
await waitForPause();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Isolate> waitForPause() async {
|
Future<void> addBreakpoint(Uri uri, int line) async {
|
||||||
_debugPrint('Waiting for isolate to pause');
|
_debugPrint('Sending breakpoint for: $uri:$line');
|
||||||
final String flutterIsolate = await _getFlutterIsolateId();
|
await _vmService.addBreakpointWithScriptUri(
|
||||||
|
await _getFlutterIsolateId(),
|
||||||
|
uri.toString(),
|
||||||
|
line,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method isn't racy. If the isolate is already paused,
|
||||||
|
// it will immediately return.
|
||||||
Future<Isolate> waitForPause() async {
|
Future<Isolate> waitForPause() async {
|
||||||
|
return _timeoutWithMessages<Isolate>(
|
||||||
|
() async {
|
||||||
|
final String flutterIsolate = await _getFlutterIsolateId();
|
||||||
final Completer<Event> pauseEvent = Completer<Event>();
|
final Completer<Event> pauseEvent = Completer<Event>();
|
||||||
|
|
||||||
// Start listening for pause events.
|
// Start listening for pause events.
|
||||||
final StreamSubscription<Event> pauseSub = _vmService.onDebugEvent
|
final StreamSubscription<Event> pauseSubscription = _vmService.onDebugEvent
|
||||||
.where((Event event) =>
|
.where((Event event) {
|
||||||
event.isolate.id == flutterIsolate &&
|
return event.isolate.id == flutterIsolate
|
||||||
event.kind.startsWith('Pause'))
|
&& event.kind.startsWith('Pause');
|
||||||
.listen(pauseEvent.complete);
|
})
|
||||||
|
.listen((Event event) {
|
||||||
|
if (!pauseEvent.isCompleted)
|
||||||
|
pauseEvent.complete(event);
|
||||||
|
});
|
||||||
|
|
||||||
// But also check if the isolate was already paused (only after we've set
|
// But also check if the isolate was already paused (only after we've set
|
||||||
// up the sub) to avoid races. If it was paused, we don't need to wait
|
// up the subscription) to avoid races. If it was paused, we don't need to wait
|
||||||
// for the event.
|
// for the event.
|
||||||
final Isolate isolate = await _vmService.getIsolate(flutterIsolate);
|
final Isolate isolate = await _vmService.getIsolate(flutterIsolate);
|
||||||
if (!isolate.pauseEvent.kind.startsWith('Pause')) {
|
if (isolate.pauseEvent.kind.startsWith('Pause')) {
|
||||||
|
_debugPrint('Isolate was already paused (${isolate.pauseEvent.kind}).');
|
||||||
|
} else {
|
||||||
|
_debugPrint('Isolate is not already paused, waiting for event to arrive...');
|
||||||
await pauseEvent.future;
|
await pauseEvent.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel the sub on either of the above.
|
// Cancel the subscription on either of the above.
|
||||||
await pauseSub.cancel();
|
await pauseSubscription.cancel();
|
||||||
|
|
||||||
return _getFlutterIsolate();
|
return _getFlutterIsolate();
|
||||||
|
},
|
||||||
|
task: 'Waiting for isolate to pause',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _timeoutWithMessages<Isolate>(waitForPause,
|
Future<Isolate> resume({bool waitForNextPause = false}) => _resume(null, waitForNextPause);
|
||||||
message: 'Isolate did not pause');
|
Future<Isolate> stepOver({bool waitForNextPause = true}) => _resume(StepOption.kOver, waitForNextPause);
|
||||||
}
|
Future<Isolate> stepOverAsync({ bool waitForNextPause = true }) => _resume(StepOption.kOverAsyncSuspension, waitForNextPause);
|
||||||
|
Future<Isolate> stepInto({bool waitForNextPause = true}) => _resume(StepOption.kInto, waitForNextPause);
|
||||||
|
Future<Isolate> stepOut({bool waitForNextPause = true}) => _resume(StepOption.kOut, waitForNextPause);
|
||||||
|
|
||||||
Future<bool> isAtAsyncSuspension() async {
|
Future<bool> isAtAsyncSuspension() async {
|
||||||
final Isolate isolate = await _getFlutterIsolate();
|
final Isolate isolate = await _getFlutterIsolate();
|
||||||
return isolate.pauseEvent.atAsyncSuspension == true;
|
return isolate.pauseEvent.atAsyncSuspension == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Isolate> resume({bool wait = true}) => _resume(wait: wait);
|
Future<Isolate> stepOverOrOverAsyncSuspension({ bool waitForNextPause = true }) async {
|
||||||
Future<Isolate> stepOver({bool wait = true}) => _resume(step: StepOption.kOver, wait: wait);
|
if (await isAtAsyncSuspension())
|
||||||
Future<Isolate> stepOverAsync({ bool wait = true }) => _resume(step: StepOption.kOverAsyncSuspension, wait: wait);
|
return await stepOverAsync(waitForNextPause: waitForNextPause);
|
||||||
Future<Isolate> stepOverOrOverAsyncSuspension({ bool wait = true }) async {
|
return await stepOver(waitForNextPause: waitForNextPause);
|
||||||
return (await isAtAsyncSuspension()) ? stepOverAsync(wait: wait) : stepOver(wait: wait);
|
|
||||||
}
|
}
|
||||||
Future<Isolate> stepInto({bool wait = true}) => _resume(step: StepOption.kInto, wait: wait);
|
|
||||||
Future<Isolate> stepOut({bool wait = true}) => _resume(step: StepOption.kOut, wait: wait);
|
|
||||||
|
|
||||||
Future<Isolate> _resume({String step, bool wait = true}) async {
|
Future<Isolate> _resume(String step, bool waitForNextPause) async {
|
||||||
_debugPrint('Sending resume ($step)');
|
assert(waitForNextPause != null);
|
||||||
await _timeoutWithMessages<dynamic>(() async => _vmService.resume(await _getFlutterIsolateId(), step: step),
|
await _timeoutWithMessages<dynamic>(
|
||||||
message: 'Isolate did not respond to resume ($step)');
|
() async => _vmService.resume(await _getFlutterIsolateId(), step: step),
|
||||||
return wait ? waitForPause() : null;
|
task: 'Resuming isolate (step=$step)',
|
||||||
|
);
|
||||||
|
return waitForNextPause ? waitForPause() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<InstanceRef> evaluateInFrame(String expression) async {
|
Future<InstanceRef> evaluateInFrame(String expression) async {
|
||||||
return _timeoutWithMessages<InstanceRef>(
|
return _timeoutWithMessages<InstanceRef>(
|
||||||
() async => await _vmService.evaluateInFrame(await _getFlutterIsolateId(), 0, expression),
|
() async => await _vmService.evaluateInFrame(await _getFlutterIsolateId(), 0, expression),
|
||||||
message: 'Timed out evaluating expression ($expression)');
|
task: 'Evaluating expression ($expression)',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<InstanceRef> evaluate(String targetId, String expression) async {
|
Future<InstanceRef> evaluate(String targetId, String expression) async {
|
||||||
return _timeoutWithMessages<InstanceRef>(
|
return _timeoutWithMessages<InstanceRef>(
|
||||||
() async => await _vmService.evaluate(await _getFlutterIsolateId(), targetId, expression),
|
() async => await _vmService.evaluate(await _getFlutterIsolateId(), targetId, expression),
|
||||||
message: 'Timed out evaluating expression ($expression for $targetId)');
|
task: 'Evaluating expression ($expression for $targetId)',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Frame> getTopStackFrame() async {
|
Future<Frame> getTopStackFrame() async {
|
||||||
@ -260,26 +313,29 @@ abstract class FlutterTestDriver {
|
|||||||
Future<Map<String, dynamic>> _waitFor({
|
Future<Map<String, dynamic>> _waitFor({
|
||||||
String event,
|
String event,
|
||||||
int id,
|
int id,
|
||||||
Duration timeout,
|
Duration timeout = defaultTimeout,
|
||||||
bool ignoreAppStopEvent = false,
|
bool ignoreAppStopEvent = false,
|
||||||
}) async {
|
}) async {
|
||||||
|
assert(timeout != null);
|
||||||
|
assert(event != null || id != null);
|
||||||
|
assert(event == null || id == null);
|
||||||
|
final String interestingOccurrence = event != null ? '$event event' : 'response to request $id';
|
||||||
final Completer<Map<String, dynamic>> response = Completer<Map<String, dynamic>>();
|
final Completer<Map<String, dynamic>> response = Completer<Map<String, dynamic>>();
|
||||||
StreamSubscription<String> sub;
|
StreamSubscription<String> subscription;
|
||||||
sub = _stdout.stream.listen((String line) async {
|
subscription = _stdout.stream.listen((String line) async {
|
||||||
final dynamic json = parseFlutterResponse(line);
|
final dynamic json = parseFlutterResponse(line);
|
||||||
_lastResponse = line;
|
_lastResponse = line;
|
||||||
if (json == null) {
|
if (json == null)
|
||||||
return;
|
return;
|
||||||
} else if (
|
if ((event != null && json['event'] == event) ||
|
||||||
(event != null && json['event'] == event)
|
(id != null && json['id'] == id)) {
|
||||||
|| (id != null && json['id'] == id)) {
|
await subscription.cancel();
|
||||||
await sub.cancel();
|
_debugPrint('OK ($interestingOccurrence)');
|
||||||
response.complete(json);
|
response.complete(json);
|
||||||
} else if (!ignoreAppStopEvent && json['event'] == 'app.stop') {
|
} else if (!ignoreAppStopEvent && json['event'] == 'app.stop') {
|
||||||
await sub.cancel();
|
await subscription.cancel();
|
||||||
final StringBuffer error = StringBuffer();
|
final StringBuffer error = StringBuffer();
|
||||||
error.write('Received app.stop event while waiting for ');
|
error.write('Received app.stop event while waiting for $interestingOccurrence\n\n');
|
||||||
error.write('${event != null ? '$event event' : 'response to request $id.'}.\n\n');
|
|
||||||
if (json['params'] != null && json['params']['error'] != null) {
|
if (json['params'] != null && json['params']['error'] != null) {
|
||||||
error.write('${json['params']['error']}\n\n');
|
error.write('${json['params']['error']}\n\n');
|
||||||
}
|
}
|
||||||
@ -290,67 +346,107 @@ abstract class FlutterTestDriver {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return _timeoutWithMessages<Map<String, dynamic>>(() => response.future,
|
return _timeoutWithMessages(
|
||||||
|
() => response.future,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
message: event != null
|
task: 'Expecting $interestingOccurrence',
|
||||||
? 'Did not receive expected $event event.'
|
).whenComplete(subscription.cancel);
|
||||||
: 'Did not receive response to request "$id".')
|
|
||||||
.whenComplete(() => sub.cancel());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<T> _timeoutWithMessages<T>(Future<T> Function() f, {Duration timeout, String message}) {
|
Future<T> _timeoutWithMessages<T>(Future<T> Function() callback, {
|
||||||
// Capture output to a buffer so if we don't get the response we want we can show
|
@required String task,
|
||||||
// the output that did arrive in the timeout error.
|
Duration timeout = defaultTimeout,
|
||||||
final StringBuffer messages = StringBuffer();
|
}) {
|
||||||
|
assert(task != null);
|
||||||
|
assert(timeout != null);
|
||||||
|
|
||||||
|
if (_printDebugOutputToStdOut) {
|
||||||
|
_debugPrint('$task...');
|
||||||
|
return callback()..timeout(timeout, onTimeout: () {
|
||||||
|
_debugPrint('$task is taking longer than usual...');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're not showing all output to the screen, so let's capture the output
|
||||||
|
// that we would have printed if we were, and output it if we take longer
|
||||||
|
// than the timeout or if we get an error.
|
||||||
|
final StringBuffer messages = StringBuffer('$task\n');
|
||||||
final DateTime start = DateTime.now();
|
final DateTime start = DateTime.now();
|
||||||
void logMessage(String m) {
|
bool timeoutExpired = false;
|
||||||
|
void logMessage(String logLine) {
|
||||||
final int ms = DateTime.now().difference(start).inMilliseconds;
|
final int ms = DateTime.now().difference(start).inMilliseconds;
|
||||||
messages.writeln('[+ ${ms.toString().padLeft(5)}] $m');
|
final String formattedLine = '[+ ${ms.toString().padLeft(5)}] $logLine';
|
||||||
|
messages.writeln(formattedLine);
|
||||||
}
|
}
|
||||||
final StreamSubscription<String> sub = _allMessages.stream.listen(logMessage);
|
final StreamSubscription<String> subscription = _allMessages.stream.listen(logMessage);
|
||||||
|
|
||||||
return f().timeout(timeout ?? defaultTimeout, onTimeout: () {
|
final Future<T> future = callback();
|
||||||
logMessage('<timed out>');
|
|
||||||
throw '$message';
|
future.timeout(timeout ?? defaultTimeout, onTimeout: () {
|
||||||
}).catchError((dynamic error) {
|
print(messages.toString());
|
||||||
throw '$error\nReceived:\n${messages.toString()}';
|
timeoutExpired = true;
|
||||||
}).whenComplete(() => sub.cancel());
|
print('$task is taking longer than usual...');
|
||||||
|
});
|
||||||
|
|
||||||
|
return future.catchError((dynamic error) {
|
||||||
|
if (!timeoutExpired) {
|
||||||
|
timeoutExpired = true;
|
||||||
|
print(messages.toString());
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}).whenComplete(() => subscription.cancel());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FlutterRunTestDriver extends FlutterTestDriver {
|
class FlutterRunTestDriver extends FlutterTestDriver {
|
||||||
FlutterRunTestDriver(Directory _projectFolder, {String logPrefix}):
|
FlutterRunTestDriver(
|
||||||
super(_projectFolder, logPrefix: logPrefix);
|
Directory projectFolder, {
|
||||||
|
String logPrefix,
|
||||||
|
}) : super(projectFolder, logPrefix: logPrefix);
|
||||||
|
|
||||||
String _currentRunningAppId;
|
String _currentRunningAppId;
|
||||||
|
|
||||||
Future<void> run({
|
Future<void> run({
|
||||||
bool withDebugger = false,
|
bool withDebugger = false,
|
||||||
|
bool startPaused = false,
|
||||||
bool pauseOnExceptions = false,
|
bool pauseOnExceptions = false,
|
||||||
File pidFile,
|
File pidFile,
|
||||||
}) async {
|
}) async {
|
||||||
await _setupProcess(<String>[
|
await _setupProcess(
|
||||||
|
<String>[
|
||||||
'run',
|
'run',
|
||||||
'--machine',
|
'--machine',
|
||||||
'-d',
|
'-d',
|
||||||
'flutter-tester',
|
'flutter-tester',
|
||||||
], withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions, pidFile: pidFile);
|
],
|
||||||
|
withDebugger: withDebugger,
|
||||||
|
startPaused: startPaused,
|
||||||
|
pauseOnExceptions: pauseOnExceptions,
|
||||||
|
pidFile: pidFile,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> attach(
|
Future<void> attach(
|
||||||
int port, {
|
int port, {
|
||||||
bool withDebugger = false,
|
bool withDebugger = false,
|
||||||
|
bool startPaused = false,
|
||||||
bool pauseOnExceptions = false,
|
bool pauseOnExceptions = false,
|
||||||
File pidFile,
|
File pidFile,
|
||||||
}) async {
|
}) async {
|
||||||
await _setupProcess(<String>[
|
await _setupProcess(
|
||||||
|
<String>[
|
||||||
'attach',
|
'attach',
|
||||||
'--machine',
|
'--machine',
|
||||||
'-d',
|
'-d',
|
||||||
'flutter-tester',
|
'flutter-tester',
|
||||||
'--debug-port',
|
'--debug-port',
|
||||||
'$port',
|
'$port',
|
||||||
], withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions, pidFile: pidFile);
|
],
|
||||||
|
withDebugger: withDebugger,
|
||||||
|
startPaused: startPaused,
|
||||||
|
pauseOnExceptions: pauseOnExceptions,
|
||||||
|
pidFile: pidFile,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -358,35 +454,35 @@ class FlutterRunTestDriver extends FlutterTestDriver {
|
|||||||
List<String> args, {
|
List<String> args, {
|
||||||
String script,
|
String script,
|
||||||
bool withDebugger = false,
|
bool withDebugger = false,
|
||||||
|
bool startPaused = false,
|
||||||
bool pauseOnExceptions = false,
|
bool pauseOnExceptions = false,
|
||||||
File pidFile,
|
File pidFile,
|
||||||
}) async {
|
}) async {
|
||||||
|
assert(!startPaused || withDebugger);
|
||||||
await super._setupProcess(
|
await super._setupProcess(
|
||||||
args,
|
args,
|
||||||
script: script,
|
script: script,
|
||||||
withDebugger: withDebugger,
|
withDebugger: withDebugger,
|
||||||
pauseOnExceptions: pauseOnExceptions,
|
|
||||||
pidFile: pidFile,
|
pidFile: pidFile,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Stash the PID so that we can terminate the VM more reliably than using
|
// Stash the PID so that we can terminate the VM more reliably than using
|
||||||
// _proc.kill() (because _proc is a shell, because `flutter` is a shell
|
// _process.kill() (`flutter` is a shell script so _process itself is a
|
||||||
// script).
|
// shell, not the flutter tool's Dart process).
|
||||||
final Map<String, dynamic> connected = await _waitFor(event: 'daemon.connected');
|
final Map<String, dynamic> connected = await _waitFor(event: 'daemon.connected');
|
||||||
_procPid = connected['params']['pid'];
|
_processPid = connected['params']['pid'];
|
||||||
|
|
||||||
// Set this up now, but we don't wait it yet. We want to make sure we don't
|
// Set this up now, but we don't wait it yet. We want to make sure we don't
|
||||||
// miss it while waiting for debugPort below.
|
// miss it while waiting for debugPort below.
|
||||||
final Future<Map<String, dynamic>> started = _waitFor(event: 'app.started',
|
final Future<Map<String, dynamic>> started = _waitFor(event: 'app.started', timeout: appStartTimeout);
|
||||||
timeout: appStartTimeout);
|
|
||||||
|
|
||||||
if (withDebugger) {
|
if (withDebugger) {
|
||||||
final Map<String, dynamic> debugPort = await _waitFor(event: 'app.debugPort',
|
final Map<String, dynamic> debugPort = await _waitFor(event: 'app.debugPort', timeout: appStartTimeout);
|
||||||
timeout: appStartTimeout);
|
|
||||||
final String wsUriString = debugPort['params']['wsUri'];
|
final String wsUriString = debugPort['params']['wsUri'];
|
||||||
_vmServiceWsUri = Uri.parse(wsUriString);
|
_vmServiceWsUri = Uri.parse(wsUriString);
|
||||||
await connectToVmService(pauseOnExceptions: pauseOnExceptions);
|
await connectToVmService(pauseOnExceptions: pauseOnExceptions);
|
||||||
await resume(wait: false);
|
if (!startPaused)
|
||||||
|
await resume(waitForNextPause: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now await the started event; if it had already happened the future will
|
// Now await the started event; if it had already happened the future will
|
||||||
@ -401,24 +497,26 @@ class FlutterRunTestDriver extends FlutterTestDriver {
|
|||||||
if (_currentRunningAppId == null)
|
if (_currentRunningAppId == null)
|
||||||
throw Exception('App has not started yet');
|
throw Exception('App has not started yet');
|
||||||
|
|
||||||
final dynamic hotReloadResp = await _sendRequest(
|
_debugPrint('Performing ${ pause ? "paused " : "" }${ fullRestart ? "hot restart" : "hot reload" }...');
|
||||||
|
final dynamic hotReloadResponse = await _sendRequest(
|
||||||
'app.restart',
|
'app.restart',
|
||||||
<String, dynamic>{'appId': _currentRunningAppId, 'fullRestart': fullRestart, 'pause': pause},
|
<String, dynamic>{'appId': _currentRunningAppId, 'fullRestart': fullRestart, 'pause': pause}
|
||||||
);
|
);
|
||||||
|
_debugPrint('${ fullRestart ? "Hot restart" : "Hot reload" } complete.');
|
||||||
|
|
||||||
if (hotReloadResp == null || hotReloadResp['code'] != 0)
|
if (hotReloadResponse == null || hotReloadResponse['code'] != 0)
|
||||||
_throwErrorResponse('Hot ${fullRestart ? 'restart' : 'reload'} request failed');
|
_throwErrorResponse('Hot ${fullRestart ? 'restart' : 'reload'} request failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> detach() async {
|
Future<int> detach() async {
|
||||||
if (_vmService != null) {
|
if (_vmService != null) {
|
||||||
_debugPrint('Closing VM service');
|
_debugPrint('Closing VM service...');
|
||||||
_vmService.dispose();
|
_vmService.dispose();
|
||||||
}
|
}
|
||||||
if (_currentRunningAppId != null) {
|
if (_currentRunningAppId != null) {
|
||||||
_debugPrint('Detaching from app');
|
_debugPrint('Detaching from app...');
|
||||||
await Future.any<void>(<Future<void>>[
|
await Future.any<void>(<Future<void>>[
|
||||||
_proc.exitCode,
|
_process.exitCode,
|
||||||
_sendRequest(
|
_sendRequest(
|
||||||
'app.detach',
|
'app.detach',
|
||||||
<String, dynamic>{'appId': _currentRunningAppId},
|
<String, dynamic>{'appId': _currentRunningAppId},
|
||||||
@ -429,19 +527,19 @@ class FlutterRunTestDriver extends FlutterTestDriver {
|
|||||||
);
|
);
|
||||||
_currentRunningAppId = null;
|
_currentRunningAppId = null;
|
||||||
}
|
}
|
||||||
_debugPrint('Waiting for process to end');
|
_debugPrint('Waiting for process to end...');
|
||||||
return _proc.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
|
return _process.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> stop() async {
|
Future<int> stop() async {
|
||||||
if (_vmService != null) {
|
if (_vmService != null) {
|
||||||
_debugPrint('Closing VM service');
|
_debugPrint('Closing VM service...');
|
||||||
_vmService.dispose();
|
_vmService.dispose();
|
||||||
}
|
}
|
||||||
if (_currentRunningAppId != null) {
|
if (_currentRunningAppId != null) {
|
||||||
_debugPrint('Stopping app');
|
_debugPrint('Stopping application...');
|
||||||
await Future.any<void>(<Future<void>>[
|
await Future.any<void>(<Future<void>>[
|
||||||
_proc.exitCode,
|
_process.exitCode,
|
||||||
_sendRequest(
|
_sendRequest(
|
||||||
'app.stop',
|
'app.stop',
|
||||||
<String, dynamic>{'appId': _currentRunningAppId},
|
<String, dynamic>{'appId': _currentRunningAppId},
|
||||||
@ -452,27 +550,13 @@ class FlutterRunTestDriver extends FlutterTestDriver {
|
|||||||
);
|
);
|
||||||
_currentRunningAppId = null;
|
_currentRunningAppId = null;
|
||||||
}
|
}
|
||||||
if (_proc != null) {
|
if (_process != null) {
|
||||||
_debugPrint('Waiting for process to end');
|
_debugPrint('Waiting for process to end...');
|
||||||
return _proc.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
|
return _process.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Isolate> breakAt(Uri uri, int line, { bool restart = false }) async {
|
|
||||||
if (restart) {
|
|
||||||
// For a hot restart, we need to send the breakpoints after the restart
|
|
||||||
// so we need to pause during the restart to avoid races.
|
|
||||||
await hotRestart(pause: true);
|
|
||||||
await addBreakpoint(uri, line);
|
|
||||||
return resume();
|
|
||||||
} else {
|
|
||||||
await addBreakpoint(uri, line);
|
|
||||||
await hotReload();
|
|
||||||
return waitForPause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int id = 1;
|
int id = 1;
|
||||||
Future<dynamic> _sendRequest(String method, dynamic params) async {
|
Future<dynamic> _sendRequest(String method, dynamic params) async {
|
||||||
final int requestId = id++;
|
final int requestId = id++;
|
||||||
@ -482,16 +566,16 @@ class FlutterRunTestDriver extends FlutterTestDriver {
|
|||||||
'params': params
|
'params': params
|
||||||
};
|
};
|
||||||
final String jsonEncoded = json.encode(<Map<String, dynamic>>[request]);
|
final String jsonEncoded = json.encode(<Map<String, dynamic>>[request]);
|
||||||
_debugPrint(jsonEncoded);
|
_debugPrint(jsonEncoded, topic: '=stdin=>');
|
||||||
|
|
||||||
// Set up the response future before we send the request to avoid any
|
// Set up the response future before we send the request to avoid any
|
||||||
// races. If the method we're calling is app.stop then we tell waitFor not
|
// races. If the method we're calling is app.stop then we tell _waitFor not
|
||||||
// to throw if it sees an app.stop event before the response to this request.
|
// to throw if it sees an app.stop event before the response to this request.
|
||||||
final Future<Map<String, dynamic>> responseFuture = _waitFor(
|
final Future<Map<String, dynamic>> responseFuture = _waitFor(
|
||||||
id: requestId,
|
id: requestId,
|
||||||
ignoreAppStopEvent: method == 'app.stop',
|
ignoreAppStopEvent: method == 'app.stop',
|
||||||
);
|
);
|
||||||
_proc.stdin.writeln(jsonEncoded);
|
_process.stdin.writeln(jsonEncoded);
|
||||||
final Map<String, dynamic> response = await responseFuture;
|
final Map<String, dynamic> response = await responseFuture;
|
||||||
|
|
||||||
if (response['error'] != null || response['result'] == null)
|
if (response['error'] != null || response['result'] == null)
|
||||||
@ -500,8 +584,8 @@ class FlutterRunTestDriver extends FlutterTestDriver {
|
|||||||
return response['result'];
|
return response['result'];
|
||||||
}
|
}
|
||||||
|
|
||||||
void _throwErrorResponse(String msg) {
|
void _throwErrorResponse(String message) {
|
||||||
throw '$msg\n\n$_lastResponse\n\n${_errorBuffer.toString()}'.trim();
|
throw '$message\n\n$_lastResponse\n\n${_errorBuffer.toString()}'.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -537,7 +621,6 @@ class FlutterTestTestDriver extends FlutterTestDriver {
|
|||||||
args,
|
args,
|
||||||
script: script,
|
script: script,
|
||||||
withDebugger: withDebugger,
|
withDebugger: withDebugger,
|
||||||
pauseOnExceptions: pauseOnExceptions,
|
|
||||||
pidFile: pidFile,
|
pidFile: pidFile,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -545,7 +628,7 @@ class FlutterTestTestDriver extends FlutterTestDriver {
|
|||||||
// _proc.kill() (because _proc is a shell, because `flutter` is a shell
|
// _proc.kill() (because _proc is a shell, because `flutter` is a shell
|
||||||
// script).
|
// script).
|
||||||
final Map<String, dynamic> version = await _waitForJson();
|
final Map<String, dynamic> version = await _waitForJson();
|
||||||
_procPid = version['pid'];
|
_processPid = version['pid'];
|
||||||
|
|
||||||
if (withDebugger) {
|
if (withDebugger) {
|
||||||
final Map<String, dynamic> startedProcess = await _waitFor(event: 'test.startedProcess', timeout: appStartTimeout);
|
final Map<String, dynamic> startedProcess = await _waitFor(event: 'test.startedProcess', timeout: appStartTimeout);
|
||||||
@ -556,7 +639,7 @@ class FlutterTestTestDriver extends FlutterTestDriver {
|
|||||||
if (beforeStart != null) {
|
if (beforeStart != null) {
|
||||||
await beforeStart();
|
await beforeStart();
|
||||||
}
|
}
|
||||||
await resume(wait: false);
|
await resume(waitForNextPause: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -566,7 +649,7 @@ class FlutterTestTestDriver extends FlutterTestDriver {
|
|||||||
return _timeoutWithMessages<Map<String, dynamic>>(
|
return _timeoutWithMessages<Map<String, dynamic>>(
|
||||||
() => _stdout.stream.map<Map<String, dynamic>>(_parseJsonResponse).first,
|
() => _stdout.stream.map<Map<String, dynamic>>(_parseJsonResponse).first,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
message: 'Did not receive any JSON.',
|
task: 'Waiting for JSON',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -587,8 +670,8 @@ Stream<String> transformToLines(Stream<List<int>> byteStream) {
|
|||||||
Map<String, dynamic> parseFlutterResponse(String line) {
|
Map<String, dynamic> parseFlutterResponse(String line) {
|
||||||
if (line.startsWith('[') && line.endsWith(']')) {
|
if (line.startsWith('[') && line.endsWith(']')) {
|
||||||
try {
|
try {
|
||||||
final Map<String, dynamic> resp = json.decode(line)[0];
|
final Map<String, dynamic> response = json.decode(line)[0];
|
||||||
return resp;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Not valid JSON, so likely some other output that was surrounded by [brackets]
|
// Not valid JSON, so likely some other output that was surrounded by [brackets]
|
||||||
return null;
|
return null;
|
||||||
|
@ -470,7 +470,7 @@ class TestTerminal extends AnsiTerminal {
|
|||||||
String bolden(String message) => '<bold>$message</bold>';
|
String bolden(String message) => '<bold>$message</bold>';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<String> get onCharInput {
|
Stream<String> get keystrokes {
|
||||||
return mockTerminalStdInStream;
|
return mockTerminalStdInStream;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,13 +153,32 @@ class MockPeer implements rpc.Peer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
final MockStdio mockStdio = MockStdio();
|
MockStdio mockStdio;
|
||||||
group('VMService', () {
|
group('VMService', () {
|
||||||
|
setUp(() {
|
||||||
|
mockStdio = MockStdio();
|
||||||
|
});
|
||||||
|
|
||||||
testUsingContext('fails connection eagerly in the connect() method', () async {
|
testUsingContext('fails connection eagerly in the connect() method', () async {
|
||||||
expect(
|
FakeAsync().run((FakeAsync time) {
|
||||||
VMService.connect(Uri.parse('http://host.invalid:9999/')),
|
bool failed = false;
|
||||||
throwsToolExit(),
|
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', () {
|
testUsingContext('refreshViews', () {
|
||||||
@ -167,7 +186,7 @@ void main() {
|
|||||||
bool done = false;
|
bool done = false;
|
||||||
final MockPeer mockPeer = MockPeer();
|
final MockPeer mockPeer = MockPeer();
|
||||||
expect(mockPeer.returnedFromSendRequest, 0);
|
expect(mockPeer.returnedFromSendRequest, 0);
|
||||||
final VMService vmService = VMService(mockPeer, null, null, const Duration(seconds: 1), null, null, null);
|
final VMService vmService = VMService(mockPeer, null, null, null, null, null);
|
||||||
vmService.getVM().then((void value) { done = true; });
|
vmService.getVM().then((void value) { done = true; });
|
||||||
expect(done, isFalse);
|
expect(done, isFalse);
|
||||||
expect(mockPeer.returnedFromSendRequest, 0);
|
expect(mockPeer.returnedFromSendRequest, 0);
|
||||||
|
Loading…
Reference in New Issue
Block a user