Commit 1acdfb79 authored by Yegor's avatar Yegor

Merge pull request #2120 from yjbanov/driver-wait-on-reads

support waiting for things to happen in Flutter Driver
parents df52a77f 7f397037
......@@ -4,20 +4,32 @@
import 'dart:async';
import 'package:vm_service_client/vm_service_client.dart';
import 'package:matcher/matcher.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'error.dart';
import 'find.dart';
import 'gesture.dart';
import 'health.dart';
import 'matcher_util.dart';
import 'message.dart';
import 'retry.dart';
final Logger _log = new Logger('FlutterDriver');
/// Computes a value.
///
/// If computation is asynchronous, the function may return a [Future].
///
/// See also [FlutterDriver.waitFor].
typedef dynamic EvaluatorFunction();
/// Drives a Flutter Application running in another process.
class FlutterDriver {
static const String _flutterExtensionMethod = 'ext.flutter_driver';
static const String _kFlutterExtensionMethod = 'ext.flutter_driver';
static const Duration _kDefaultTimeout = const Duration(seconds: 5);
static const Duration _kDefaultPauseBetweenRetries = const Duration(milliseconds: 160);
/// Connects to a Flutter application.
///
......@@ -62,7 +74,7 @@ class FlutterDriver {
// Waits for a signal from the VM service that the extension is registered
Future waitForServiceExtension() {
return isolate.onExtensionAdded.firstWhere((String extension) {
return extension == _flutterExtensionMethod;
return extension == _kFlutterExtensionMethod;
});
}
......@@ -124,7 +136,7 @@ class FlutterDriver {
Future<Map<String, dynamic>> _sendCommand(Command command) async {
Map<String, dynamic> json = <String, dynamic>{'kind': command.kind}
..addAll(command.toJson());
return _appIsolate.invokeExtension(_flutterExtensionMethod, json)
return _appIsolate.invokeExtension(_kFlutterExtensionMethod, json)
.then((Map<String, dynamic> result) => result, onError: (error, stackTrace) {
throw new DriverError(
'Failed to fulfill ${command.runtimeType} due to remote error',
......@@ -152,6 +164,24 @@ class FlutterDriver {
return result.text;
}
/// Calls the [evaluator] repeatedly until the result of the evaluation
/// satisfies the [matcher].
///
/// Returns the result of the evaluation.
Future<String> waitFor(EvaluatorFunction evaluator, Matcher matcher, {
Duration timeout: _kDefaultTimeout,
Duration pauseBetweenRetries: _kDefaultPauseBetweenRetries
}) async {
return retry(() async {
dynamic value = await evaluator();
MatchResult matchResult = match(value, matcher);
if (!matchResult.hasMatched) {
return new Future.error(matchResult.mismatchDescription);
}
return value;
}, timeout, pauseBetweenRetries);
}
/// Closes the underlying connection to the VM service.
///
/// Returns a [Future] that fires once the connection has been closed.
......
......@@ -13,8 +13,11 @@ import 'find.dart';
import 'gesture.dart';
import 'health.dart';
import 'message.dart';
import 'retry.dart';
const String _extensionMethod = 'ext.flutter_driver';
const Duration _kDefaultTimeout = const Duration(seconds: 5);
const Duration _kDefaultPauseBetweenRetries = const Duration(milliseconds: 160);
bool _flutterDriverExtensionEnabled = false;
......@@ -93,8 +96,11 @@ class FlutterDriverExtension {
Future<Health> getHealth(GetHealth command) async => new Health(HealthStatus.ok);
Future<ObjectRef> findByValueKey(FindByValueKey command) {
Element elem = prober.findElementByKey(new ValueKey<dynamic>(command.keyValue));
Future<ObjectRef> findByValueKey(FindByValueKey command) async {
Element elem = await retry(() {
return prober.findElementByKey(new ValueKey<dynamic>(command.keyValue));
}, _kDefaultTimeout, _kDefaultPauseBetweenRetries);
ObjectRef elemRef = elem != null
? new ObjectRef(_registerObject(elem))
: new ObjectRef.notFound();
......
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:matcher/matcher.dart';
/// Matches [value] against the [matcher].
MatchResult match(dynamic value, Matcher matcher) {
if (matcher.matches(value, {})) {
return new MatchResult._matched();
} else {
Description description =
matcher.describeMismatch(value, new _TextDescription(), {}, false);
return new MatchResult._mismatched(description.toString());
}
}
/// Result of matching a value against a matcher.
class MatchResult {
MatchResult._matched()
: hasMatched = true,
mismatchDescription = null;
MatchResult._mismatched(String mismatchDescription)
: hasMatched = false,
mismatchDescription = mismatchDescription;
/// Whether the match succeeded.
final bool hasMatched;
/// If the match did not succeed, this field contains the explanation.
final String mismatchDescription;
}
/// Writes description into a string.
class _TextDescription implements Description {
final StringBuffer _text = new StringBuffer();
int get length => _text.length;
Description add(String text) {
_text.write(text);
return this;
}
Description replace(String text) {
_text.clear();
_text.write(text);
return this;
}
Description addDescriptionOf(value) {
if (value is Matcher) {
value.describe(this);
return this;
} else {
return add('$value');
}
}
Description addAll(String start, String separator, String end, Iterable list) {
add(start);
if (list.isNotEmpty) {
addDescriptionOf(list.first);
for (dynamic item in list.skip(1)) {
add(separator);
addDescriptionOf(item);
}
}
add(end);
return this;
}
String toString() => '$_text';
}
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
/// Performs an action and returns either the result of the action or a [Future]
/// that evaluates to the result.
typedef dynamic Action();
/// Performs [action] repeatedly until it either succeeds or [timeout] limit is
/// reached.
///
/// When the retry time out, the last seen error and stack trace are returned in
/// an error [Future].
Future<dynamic> retry(Action action, Duration timeout,
Duration pauseBetweenRetries) async {
assert(action != null);
assert(timeout != null);
assert(pauseBetweenRetries != null);
Stopwatch sw = new Stopwatch()..start();
dynamic result = null;
dynamic lastError = null;
dynamic lastStackTrace = null;
bool success = false;
while(!success && sw.elapsed < timeout) {
try {
result = await action();
success = true;
} catch(error, stackTrace) {
lastError = error;
lastStackTrace = stackTrace;
if (sw.elapsed < timeout) {
await new Future<Null>.delayed(pauseBetweenRetries);
}
}
}
if (success)
return result;
else
return new Future.error(lastError, lastStackTrace);
}
......@@ -8,9 +8,9 @@ environment:
sdk: '>=1.12.0 <2.0.0'
dependencies:
vm_service_client: '>=0.1.2 <1.0.0'
json_rpc_2: any
logging: '>=0.11.0 <1.0.0'
matcher: '>=0.12.0 <1.0.0'
vm_service_client: '>=0.1.2 <1.0.0'
flutter:
path: '../flutter'
flutter_test:
......@@ -19,3 +19,4 @@ dependencies:
dev_dependencies:
test: '>=0.12.6 <1.0.0'
mockito: ^0.10.1
quiver: '>=0.21.4 <0.22.0'
......@@ -10,6 +10,7 @@ import 'package:flutter_driver/src/health.dart';
import 'package:flutter_driver/src/message.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:mockito/mockito.dart';
import 'package:quiver/testing/async.dart';
import 'package:vm_service_client/vm_service_client.dart';
main() {
......@@ -181,6 +182,58 @@ main() {
expect(result, 'hello');
});
});
group('waitFor', () {
test('waits for a condition', () {
expect(
driver.waitFor(() {
return new Future.delayed(
new Duration(milliseconds: 50),
() => 123
);
}, equals(123)),
completion(123)
);
});
test('retries a correct number of times', () {
new FakeAsync().run((FakeAsync fakeAsync) {
int retryCount = 0;
expect(
driver.waitFor(
() {
retryCount++;
return retryCount;
},
equals(2),
timeout: new Duration(milliseconds: 30),
pauseBetweenRetries: new Duration(milliseconds: 10)
),
completion(2)
);
fakeAsync.elapse(new Duration(milliseconds: 50));
// Check that we didn't retry more times than necessary
expect(retryCount, 2);
});
});
test('times out', () async {
bool timedOut = false;
await driver.waitFor(
() => 1,
equals(2),
timeout: new Duration(milliseconds: 10),
pauseBetweenRetries: new Duration(milliseconds: 2)
).catchError((err, stack) {
timedOut = true;
});
expect(timedOut, isTrue);
});
});
});
}
......
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/test.dart';
import 'package:quiver/testing/async.dart';
import 'package:flutter_driver/src/retry.dart';
main() {
group('retry', () {
test('retries until succeeds', () {
new FakeAsync().run((FakeAsync fakeAsync) {
int retryCount = 0;
expect(
retry(
() async {
retryCount++;
if (retryCount < 2) {
throw 'error';
} else {
return retryCount;
}
},
new Duration(milliseconds: 30),
new Duration(milliseconds: 10)
),
completion(2)
);
fakeAsync.elapse(new Duration(milliseconds: 50));
// Check that we didn't retry more times than necessary
expect(retryCount, 2);
});
});
test('times out returning last error', () async {
bool timedOut = false;
int retryCount = 0;
dynamic lastError;
dynamic lastStackTrace;
await retry(
() {
retryCount++;
throw 'error';
},
new Duration(milliseconds: 9),
new Duration(milliseconds: 2)
).catchError((error, stackTrace) {
timedOut = true;
lastError = error;
lastStackTrace = stackTrace;
});
expect(timedOut, isTrue);
expect(lastError, 'error');
expect(lastStackTrace, isNotNull);
expect(retryCount, 4);
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment