Commit be7be2b8 authored by Ian Hickson's avatar Ian Hickson Committed by Adam Barth

Test service extensions (#7849)

...and fix bugs that the tests uncovered.

WRITE TEST FIND BUG
parent cce70d70
......@@ -2,9 +2,16 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
class TestTestBinding extends AutomatedTestWidgetsFlutterBinding {
@override
DebugPrintCallback get debugPrintOverride => testPrint;
static void testPrint(String message, { int wrapWidth }) { print(message); }
}
Future<Null> guardedHelper(WidgetTester tester) {
return TestAsyncUtils.guard(() async {
await tester.pumpWidget(new Text('Hello'));
......@@ -12,8 +19,8 @@ Future<Null> guardedHelper(WidgetTester tester) {
}
void main() {
new TestTestBinding();
testWidgets('TestAsyncUtils - custom guarded sections', (WidgetTester tester) async {
debugPrint = (String message, { int wrapWidth }) { print(message); };
await tester.pumpWidget(new Container());
expect(find.byElementType(Container), isNotNull);
guardedHelper(tester);
......
......@@ -2,16 +2,22 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/widgets.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
class TestTestBinding extends AutomatedTestWidgetsFlutterBinding {
@override
DebugPrintCallback get debugPrintOverride => testPrint;
static void testPrint(String message, { int wrapWidth }) { print(message); }
}
Future<Null> helperFunction(WidgetTester tester) async {
await tester.pump();
}
void main() {
new TestTestBinding();
testWidgets('TestAsyncUtils - handling unguarded async helper functions', (WidgetTester tester) async {
debugPrint = (String message, { int wrapWidth }) { print(message); };
helperFunction(tester);
helperFunction(tester);
// this should fail
......
......@@ -46,7 +46,7 @@ class TestAssetBundle extends AssetBundle {
}
@override
Future<dynamic> loadStructuredData<T>(String key, Future<T> parser(String value)) async {
Future<T> loadStructuredData<T>(String key, Future<T> parser(String value)) async {
return parser(await loadString(key));
}
......
......@@ -13,6 +13,7 @@ export 'src/foundation/assertions.dart';
export 'src/foundation/basic_types.dart';
export 'src/foundation/binding.dart';
export 'src/foundation/change_notifier.dart';
export 'src/foundation/debug.dart';
export 'src/foundation/licenses.dart';
export 'src/foundation/observer_list.dart';
export 'src/foundation/platform.dart';
......
......@@ -19,7 +19,7 @@ import 'basic_types.dart';
/// "type" key will be set to the string `_extensionType` to indicate
/// that this is a return value from a service extension, and the
/// "method" key will be set to the full name of the method.
typedef Future<Map<String, dynamic>> ServiceExtensionCallback(Map<String, String> parameters);
typedef Future<Map<String, String>> ServiceExtensionCallback(Map<String, String> parameters);
/// Base class for mixins that provide singleton services (also known as
/// "bindings").
......@@ -56,7 +56,7 @@ abstract class BindingBase {
initServiceExtensions();
assert(_debugServiceExtensionsRegistered);
developer.postEvent('Flutter.FrameworkInitialization', <String, dynamic>{});
developer.postEvent('Flutter.FrameworkInitialization', <String, String>{});
developer.Timeline.finishSync();
}
......@@ -150,7 +150,7 @@ abstract class BindingBase {
name: name,
callback: (Map<String, String> parameters) async {
await callback();
return <String, dynamic>{};
return <String, String>{};
}
);
}
......@@ -181,7 +181,7 @@ abstract class BindingBase {
callback: (Map<String, String> parameters) async {
if (parameters.containsKey('enabled'))
await setter(parameters['enabled'] == 'true');
return <String, dynamic>{ 'enabled': await getter() };
return <String, String>{ 'enabled': await getter() ? 'true' : 'false' };
}
);
}
......@@ -211,7 +211,7 @@ abstract class BindingBase {
callback: (Map<String, String> parameters) async {
if (parameters.containsKey(name))
await setter(double.parse(parameters[name]));
return <String, dynamic>{ name: await getter() };
return <String, String>{ name: (await getter()).toString() };
}
);
}
......@@ -240,7 +240,7 @@ abstract class BindingBase {
callback: (Map<String, String> parameters) async {
if (parameters.containsKey('value'))
await setter(parameters['value']);
return <String, dynamic>{ 'value': await getter() };
return <String, String>{ 'value': await getter() };
}
);
}
......@@ -267,7 +267,7 @@ abstract class BindingBase {
assert(method == methodName);
dynamic caughtException;
StackTrace caughtStack;
Map<String, dynamic> result;
Map<String, String> result;
try {
result = await callback(parameters);
} catch (exception, stack) {
......@@ -286,10 +286,10 @@ abstract class BindingBase {
));
return new developer.ServiceExtensionResponse.error(
developer.ServiceExtensionResponse.extensionError,
JSON.encode(<String, dynamic>{
JSON.encode(<String, String>{
'exception': caughtException.toString(),
'stack': caughtStack.toString(),
'method': method
'method': method,
})
);
}
......
// Copyright 2017 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 'assertions.dart';
import 'print.dart';
/// Returns true if none of the foundation library debug variables have been
/// changed.
///
/// This function is used by the test framework to ensure that debug variables
/// haven't been inadvertently changed.
///
/// The `debugPrintOverride` argument can be specified to indicate the expected
/// value of the [debugPrint] variable. This is useful for test frameworks that
/// override [debugPrint] themselves and want to check that their own custom
/// value wasn't overridden by a test.
///
/// See [https://docs.flutter.io/flutter/foundation/foundation-library.html] for
/// a complete list.
bool debugAssertAllFoundationVarsUnset(String reason, { DebugPrintCallback debugPrintOverride: debugPrintThrottled }) {
assert(() {
if (debugPrint != debugPrintOverride)
throw new FlutterError(reason);
return true;
});
return true;
}
......@@ -57,8 +57,7 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
if (debugPaintSizeEnabled == value)
return new Future<Null>.value();
debugPaintSizeEnabled = value;
_forceRepaint();
return endOfFrame;
return _forceRepaint();
}
);
return true;
......@@ -78,8 +77,8 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
bool repaint = debugRepaintRainbowEnabled && !value;
debugRepaintRainbowEnabled = value;
if (repaint)
_forceRepaint();
return endOfFrame;
return _forceRepaint();
return new Future<Null>.value();
}
);
return true;
......@@ -249,13 +248,14 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
super.hitTest(result, position); // ignore: abstract_super_member_reference
}
void _forceRepaint() {
Future<Null> _forceRepaint() {
RenderObjectVisitor visitor;
visitor = (RenderObject child) {
child.markNeedsPaint();
child.visitChildren(visitor);
};
instance?.renderView?.visitChildren(visitor);
return endOfFrame;
}
}
......
......@@ -66,7 +66,7 @@ abstract class AssetBundle {
///
/// Implementations may cache the result, so a particular key should only be
/// used with one parser for the lifetime of the asset bundle.
Future<dynamic> loadStructuredData<T>(String key, Future<T> parser(String value));
Future<T> loadStructuredData<T>(String key, Future<T> parser(String value));
/// If this is a caching asset bundle, and the given key describes a cached
/// asset, then evict the asset from the cache so that the next time it is
......@@ -110,7 +110,7 @@ class NetworkAssetBundle extends AssetBundle {
/// The result is not cached. The parser is run each time the resource is
/// fetched.
@override
Future<dynamic> loadStructuredData<T>(String key, Future<T> parser(String value)) async {
Future<T> loadStructuredData<T>(String key, Future<T> parser(String value)) async {
assert(key != null);
assert(parser != null);
return parser(await loadString(key));
......@@ -159,7 +159,7 @@ abstract class CachingAssetBundle extends AssetBundle {
/// subsequent calls will be a [SynchronousFuture], which resolves its
/// callback synchronously.
@override
Future<dynamic> loadStructuredData<T>(String key, Future<T> parser(String value)) {
Future<T> loadStructuredData<T>(String key, Future<T> parser(String value)) {
assert(key != null);
assert(parser != null);
if (_structuredDataCache.containsKey(key))
......
......@@ -86,8 +86,7 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
if (WidgetsApp.showPerformanceOverlayOverride == value)
return new Future<Null>.value();
WidgetsApp.showPerformanceOverlayOverride = value;
buildOwner.reassemble(renderViewElement);
return endOfFrame;
return _forceRebuild();
}
);
......@@ -98,10 +97,17 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
if (WidgetsApp.debugAllowBannerOverride == value)
return new Future<Null>.value();
WidgetsApp.debugAllowBannerOverride = value;
return _forceRebuild();
}
);
}
Future<Null> _forceRebuild() {
if (renderViewElement != null) {
buildOwner.reassemble(renderViewElement);
return endOfFrame;
}
);
return new Future<Null>.value();
}
/// The [BuildOwner] in charge of executing the build pipeline for the
......
// Copyright 2017 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';
import 'dart:convert';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:test/test.dart';
class TestServiceExtensionsBinding extends BindingBase
with SchedulerBinding,
ServicesBinding,
GestureBinding,
RendererBinding,
WidgetsBinding {
final Map<String, ServiceExtensionCallback> extensions = <String, ServiceExtensionCallback>{};
@override
void registerServiceExtension({
@required String name,
@required ServiceExtensionCallback callback
}) {
expect(extensions.containsKey(name), isFalse);
extensions[name] = callback;
}
Future<Map<String, String>> testExtension(String name, Map<String, String> arguments) {
expect(extensions.containsKey(name), isTrue);
return extensions[name](arguments);
}
int reassembled = 0;
@override
Future<Null> reassembleApplication() {
reassembled += 1;
return super.reassembleApplication();
}
bool frameScheduled = false;
@override
void scheduleFrame() {
frameScheduled = true;
}
void doFrame() {
frameScheduled = false;
if (ui.window.onBeginFrame != null)
ui.window.onBeginFrame(const Duration());
}
Future<Null> flushMicrotasks() {
Completer<Null> completer = new Completer<Null>();
new Timer(const Duration(), () {
completer.complete();
});
return completer.future;
}
}
void main() {
TestServiceExtensionsBinding binding;
List<String> console = <String>[];
test('Service extensions - pretest', () async {
binding = new TestServiceExtensionsBinding();
expect(binding.frameScheduled, isTrue);
binding.doFrame(); // initial frame scheduled by creating the binding
expect(binding.frameScheduled, isFalse);
expect(debugPrint, equals(debugPrintThrottled));
debugPrint = (String message, { int wrapWidth }) {
console.add(message);
};
});
// The following list is alphabetical, one test per extension.
//
// The order doesn't really matter except that the pretest and posttest tests
// must be first and last respectively.
test('Service extensions - debugAllowBanner', () async {
Map<String, String> result;
expect(binding.frameScheduled, isFalse);
expect(WidgetsApp.debugAllowBannerOverride, true);
result = await binding.testExtension('debugAllowBanner', <String, String>{});
expect(result, <String, String>{ 'enabled': 'true' });
expect(WidgetsApp.debugAllowBannerOverride, true);
result = await binding.testExtension('debugAllowBanner', <String, String>{ 'enabled': 'false' });
expect(result, <String, String>{ 'enabled': 'false' });
expect(WidgetsApp.debugAllowBannerOverride, false);
result = await binding.testExtension('debugAllowBanner', <String, String>{});
expect(result, <String, String>{ 'enabled': 'false' });
expect(WidgetsApp.debugAllowBannerOverride, false);
result = await binding.testExtension('debugAllowBanner', <String, String>{ 'enabled': 'true' });
expect(result, <String, String>{ 'enabled': 'true' });
expect(WidgetsApp.debugAllowBannerOverride, true);
result = await binding.testExtension('debugAllowBanner', <String, String>{});
expect(result, <String, String>{ 'enabled': 'true' });
expect(WidgetsApp.debugAllowBannerOverride, true);
expect(binding.frameScheduled, isFalse);
});
test('Service extensions - debugDumpApp', () async {
Map<String, String> result;
result = await binding.testExtension('debugDumpApp', <String, String>{});
expect(result, <String, String>{});
expect(console, <String>['TestServiceExtensionsBinding - CHECKED MODE', '<no tree currently mounted>']);
console.clear();
});
test('Service extensions - debugDumpRenderTree', () async {
Map<String, String> result;
result = await binding.testExtension('debugDumpRenderTree', <String, String>{});
expect(result, <String, String>{});
expect(console, <String>[
'RenderView\n'
' debug mode enabled - linux\n'
' window size: Size(800.0, 600.0) (in physical pixels)\n'
' device pixel ratio: 1.0 (physical pixels per logical pixel)\n'
' configuration: Size(800.0, 600.0) at 1.0x (in logical pixels)\n'
'\n'
]);
console.clear();
});
test('Service extensions - debugPaint', () async {
Map<String, String> result;
Future<Map<String, String>> pendingResult;
bool completed;
expect(binding.frameScheduled, isFalse);
expect(debugPaintSizeEnabled, false);
result = await binding.testExtension('debugPaint', <String, String>{});
expect(result, <String, String>{ 'enabled': 'false' });
expect(debugPaintSizeEnabled, false);
expect(binding.frameScheduled, isFalse);
pendingResult = binding.testExtension('debugPaint', <String, String>{ 'enabled': 'true' });
completed = false;
pendingResult.whenComplete(() { completed = true; });
await binding.flushMicrotasks();
expect(binding.frameScheduled, isTrue);
expect(completed, isFalse);
binding.doFrame();
await binding.flushMicrotasks();
expect(completed, isTrue);
expect(binding.frameScheduled, isFalse);
result = await pendingResult;
expect(result, <String, String>{ 'enabled': 'true' });
expect(debugPaintSizeEnabled, true);
result = await binding.testExtension('debugPaint', <String, String>{});
expect(result, <String, String>{ 'enabled': 'true' });
expect(debugPaintSizeEnabled, true);
expect(binding.frameScheduled, isFalse);
pendingResult = binding.testExtension('debugPaint', <String, String>{ 'enabled': 'false' });
await binding.flushMicrotasks();
expect(binding.frameScheduled, isTrue);
binding.doFrame();
expect(binding.frameScheduled, isFalse);
result = await pendingResult;
expect(result, <String, String>{ 'enabled': 'false' });
expect(debugPaintSizeEnabled, false);
result = await binding.testExtension('debugPaint', <String, String>{});
expect(result, <String, String>{ 'enabled': 'false' });
expect(debugPaintSizeEnabled, false);
expect(binding.frameScheduled, isFalse);
});
test('Service extensions - evict', () async {
Map<String, String> result;
bool completed;
completed = false;
PlatformMessages.setMockBinaryMessageHandler('flutter/assets', (ByteData message) async {
expect(UTF8.decode(message.buffer.asUint8List()), 'test');
completed = true;
return new ByteData(5); // 0x0000000000
});
bool data;
data = await rootBundle.loadStructuredData<bool>('test', (String value) async { expect(value, '\x00\x00\x00\x00\x00'); return true; });
expect(data, isTrue);
expect(completed, isTrue);
completed = false;
data = await rootBundle.loadStructuredData('test', (String value) async { expect(true, isFalse); return null; });
expect(data, isTrue);
expect(completed, isFalse);
result = await binding.testExtension('evict', <String, String>{ 'value': 'test' });
expect(result, <String, String>{ 'value': '' });
expect(completed, isFalse);
data = await rootBundle.loadStructuredData<bool>('test', (String value) async { expect(value, '\x00\x00\x00\x00\x00'); return false; });
expect(data, isFalse);
expect(completed, isTrue);
PlatformMessages.setMockBinaryMessageHandler('flutter/assets', null);
});
test('Service extensions - exit', () async {
// no test for _calling_ 'exit', because that should terminate the process!
expect(binding.extensions.containsKey('exit'), isTrue);
});
test('Service extensions - frameworkPresent', () async {
Map<String, String> result;
result = await binding.testExtension('frameworkPresent', <String, String>{});
expect(result, <String, String>{});
});
test('Service extensions - repaintRainbow', () async {
Map<String, String> result;
Future<Map<String, String>> pendingResult;
bool completed;
expect(binding.frameScheduled, isFalse);
expect(debugRepaintRainbowEnabled, false);
result = await binding.testExtension('repaintRainbow', <String, String>{});
expect(result, <String, String>{ 'enabled': 'false' });
expect(debugRepaintRainbowEnabled, false);
expect(binding.frameScheduled, isFalse);
pendingResult = binding.testExtension('repaintRainbow', <String, String>{ 'enabled': 'true' });
completed = false;
pendingResult.whenComplete(() { completed = true; });
await binding.flushMicrotasks();
expect(completed, true);
expect(binding.frameScheduled, isFalse);
result = await pendingResult;
expect(result, <String, String>{ 'enabled': 'true' });
expect(debugRepaintRainbowEnabled, true);
result = await binding.testExtension('repaintRainbow', <String, String>{});
expect(result, <String, String>{ 'enabled': 'true' });
expect(debugRepaintRainbowEnabled, true);
expect(binding.frameScheduled, isFalse);
pendingResult = binding.testExtension('repaintRainbow', <String, String>{ 'enabled': 'false' });
completed = false;
pendingResult.whenComplete(() { completed = true; });
await binding.flushMicrotasks();
expect(completed, false);
expect(binding.frameScheduled, isTrue);
binding.doFrame();
await binding.flushMicrotasks();
expect(completed, true);
expect(binding.frameScheduled, isFalse);
result = await pendingResult;
expect(result, <String, String>{ 'enabled': 'false' });
expect(debugRepaintRainbowEnabled, false);
result = await binding.testExtension('repaintRainbow', <String, String>{});
expect(result, <String, String>{ 'enabled': 'false' });
expect(debugRepaintRainbowEnabled, false);
expect(binding.frameScheduled, isFalse);
});
test('Service extensions - reassemble', () async {
Map<String, String> result;
Future<Map<String, String>> pendingResult;
bool completed;
completed = false;
expect(binding.reassembled, 0);
pendingResult = binding.testExtension('reassemble', <String, String>{});
pendingResult.whenComplete(() { completed = true; });
await binding.flushMicrotasks();
expect(binding.frameScheduled, isTrue);
expect(completed, false);
binding.doFrame();
await binding.flushMicrotasks();
expect(completed, true);
expect(binding.frameScheduled, isFalse);
result = await pendingResult;
expect(result, <String, String>{});
expect(binding.reassembled, 1);
});
test('Service extensions - showPerformanceOverlay', () async {
Map<String, String> result;
expect(binding.frameScheduled, isFalse);
expect(WidgetsApp.showPerformanceOverlayOverride, false);
result = await binding.testExtension('showPerformanceOverlay', <String, String>{});
expect(result, <String, String>{ 'enabled': 'false' });
expect(WidgetsApp.showPerformanceOverlayOverride, false);
result = await binding.testExtension('showPerformanceOverlay', <String, String>{ 'enabled': 'true' });
expect(result, <String, String>{ 'enabled': 'true' });
expect(WidgetsApp.showPerformanceOverlayOverride, true);
result = await binding.testExtension('showPerformanceOverlay', <String, String>{});
expect(result, <String, String>{ 'enabled': 'true' });
expect(WidgetsApp.showPerformanceOverlayOverride, true);
result = await binding.testExtension('showPerformanceOverlay', <String, String>{ 'enabled': 'false' });
expect(result, <String, String>{ 'enabled': 'false' });
expect(WidgetsApp.showPerformanceOverlayOverride, false);
result = await binding.testExtension('showPerformanceOverlay', <String, String>{});
expect(result, <String, String>{ 'enabled': 'false' });
expect(WidgetsApp.showPerformanceOverlayOverride, false);
expect(binding.frameScheduled, isFalse);
});
test('Service extensions - timeDilation', () async {
Map<String, String> result;
expect(binding.frameScheduled, isFalse);
expect(timeDilation, 1.0);
result = await binding.testExtension('timeDilation', <String, String>{});
expect(result, <String, String>{ 'timeDilation': '1.0' });
expect(timeDilation, 1.0);
result = await binding.testExtension('timeDilation', <String, String>{ 'timeDilation': '100.0' });
expect(result, <String, String>{ 'timeDilation': '100.0' });
expect(timeDilation, 100.0);
result = await binding.testExtension('timeDilation', <String, String>{});
expect(result, <String, String>{ 'timeDilation': '100.0' });
expect(timeDilation, 100.0);
result = await binding.testExtension('timeDilation', <String, String>{ 'timeDilation': '1.0' });
expect(result, <String, String>{ 'timeDilation': '1.0' });
expect(timeDilation, 1.0);
result = await binding.testExtension('timeDilation', <String, String>{});
expect(result, <String, String>{ 'timeDilation': '1.0' });
expect(timeDilation, 1.0);
expect(binding.frameScheduled, isFalse);
});
test('Service extensions - posttest', () async {
// If you add a service extension... TEST IT! :-)
// ...then increment this number.
expect(binding.extensions.length, 11);
expect(console, isEmpty);
debugPrint = debugPrintThrottled;
});
}
......@@ -86,6 +86,14 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
RendererBinding,
// Services binding omitted to avoid dragging in the licenses code.
WidgetsBinding {
TestWidgetsFlutterBinding() {
debugPrint = debugPrintOverride;
}
@protected
DebugPrintCallback get debugPrintOverride => debugPrint;
/// Creates and initializes the binding. This function is
/// idempotent; calling it a second time will just return the
/// previously-created instance.
......@@ -399,6 +407,10 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
assert(debugAssertNoTransientCallbacks(
'An animation is still running even after the widget tree was disposed.'
));
assert(debugAssertAllFoundationVarsUnset(
'The value of a foundation debug variable was changed by the test.',
debugPrintOverride: debugPrintOverride,
));
assert(debugAssertAllRenderVarsUnset(
'The value of a rendering debug variable was changed by the test.'
));
......@@ -431,7 +443,6 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
@override
void initInstances() {
debugPrint = debugPrintSynchronously;
super.initInstances();
ui.window.onBeginFrame = null;
}
......@@ -439,6 +450,9 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
FakeAsync _fakeAsync;
Clock _clock;
@override
DebugPrintCallback get debugPrintOverride => debugPrintSynchronously;
@override
test_package.Timeout get defaultTestTimeout => const test_package.Timeout(const Duration(seconds: 5));
......
......@@ -82,7 +82,7 @@ Future<Null> _testFile(String testName, int wantedExitCode, String workingDirect
expect(haveSeenStdErrMarker, isFalse);
haveSeenStdErrMarker = true;
}
expect(outputLine, matches(expectationLine));
expect(outputLine, matches(expectationLine), verbose: true, reason: 'Full output:\n- - - -----8<----- - - -\n${output.join("\n")}\n- - - -----8<----- - - -');
expectationLineNumber += 1;
outputLineNumber += 1;
}
......
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