Commit e1adc525 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Option to enable the performance overlay from 'flutter run'. (#11288)

parent 77b0c1da
// Copyright (c) 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:io';
import 'package:path/path.dart' as path;
import 'package:flutter_devicelab/framework/adb.dart';
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/utils.dart';
void main() {
task(() async {
final Device device = await devices.workingDevice;
await device.unlock();
final Directory appDir = dir(path.join(flutterDirectory.path, 'dev/integration_tests/ui'));
await inDirectory(appDir, () async {
final Completer<Null> ready = new Completer<Null>();
bool ok;
print('run: starting...');
final Process run = await startProcess(
path.join(flutterDirectory.path, 'bin', 'flutter'),
<String>['run', '--verbose', '--observatory-port=8888', '-d', device.deviceId, 'lib/commands.dart'],
);
run.stdout
.transform(UTF8.decoder)
.transform(const LineSplitter())
.listen((String line) {
print('run:stdout: $line');
if (line.contains(new RegExp(r'^\[\s+\] For a more detailed help message, press "h"\. To quit, press "q"\.'))) {
print('run: ready!');
ready.complete();
ok ??= true;
}
});
run.stderr
.transform(UTF8.decoder)
.transform(const LineSplitter())
.listen((String line) {
stderr.writeln('run:stderr: $line');
});
run.exitCode.then((int exitCode) { ok = false; });
await Future.any<dynamic>(<Future<dynamic>>[ ready.future, run.exitCode ]);
if (!ok)
throw 'Failed to run test app.';
await drive('none');
print('test: pressing "p" to enable debugPaintSize...');
run.stdin.write('p');
await drive('debug_paint');
print('test: pressing "p" again...');
run.stdin.write('p');
await drive('none');
print('test: pressing "P" to enable performance overlay...');
run.stdin.write('P');
await drive('performance_overlay');
print('test: pressing "P" again...');
run.stdin.write('P');
await drive('none');
run.stdin.write('q');
final int result = await run.exitCode;
if (result != 0)
throw 'Received unexpected exit code $result from run process.';
});
return new TaskResult.success(null);
});
}
Future<Null> drive(String name) async {
print('drive: running commands_$name check...');
final Process drive = await startProcess(
path.join(flutterDirectory.path, 'bin', 'flutter'),
<String>['drive', '--use-existing-app', 'http://127.0.0.1:8888/', '--keep-app-running', '--driver', 'test_driver/commands_${name}_test.dart'],
);
drive.stdout
.transform(UTF8.decoder)
.transform(const LineSplitter())
.listen((String line) {
print('drive:stdout: $line');
});
drive.stderr
.transform(UTF8.decoder)
.transform(const LineSplitter())
.listen((String line) {
stderr.writeln('drive:stderr: $line');
});
final int result = await drive.exitCode;
if (result != 0)
throw 'Failed to drive test app (exit code $result).';
print('drive: finished commands_$name check successfully.');
}
\ No newline at end of file
......@@ -134,7 +134,8 @@ class _TaskRunner {
// are catching errors coming from arbitrary (and untrustworthy) task
// code. Our goal is to convert the failure into a readable message.
// Propagating it further is not useful.
completer.complete(new TaskResult.failure(message));
if (!completer.isCompleted)
completer.complete(new TaskResult.failure(message));
});
return completer.future;
}
......
......@@ -114,6 +114,13 @@ tasks:
stage: devicelab
required_agent_capabilities: ["has-android-device"]
commands_test:
description: >
Runs tests of flutter run commands.
stage: devicelab
required_agent_capabilities: ["has-android-device"]
flaky: true
android_sample_catalog_generator:
description: >
Builds sample catalog markdown pages and Android screenshots
......@@ -131,7 +138,6 @@ tasks:
Verifies that `flutter drive --route` still works. No performance numbers.
stage: devicelab
required_agent_capabilities: ["linux/android"]
flaky: true
flutter_gallery_instrumentation_test:
description: >
......@@ -140,7 +146,6 @@ tasks:
test can run on off-the-shelf infrastructures, such as Firebase Test Lab.
stage: devicelab
required_agent_capabilities: ["linux/android"]
flaky: true
# iOS on-device tests
......@@ -149,14 +154,12 @@ tasks:
Checks that platform channels work on iOS.
stage: devicelab_ios
required_agent_capabilities: ["has-ios-device"]
flaky: true
platform_channel_sample_test_ios:
description: >
Runs a driver test on the Platform Channel sample app on iOS.
stage: devicelab_ios
required_agent_capabilities: ["has-ios-device"]
flaky: true
complex_layout_scroll_perf_ios__timeline_summary:
description: >
......@@ -164,7 +167,6 @@ tasks:
iOS.
stage: devicelab_ios
required_agent_capabilities: ["has-ios-device"]
flaky: true
# flutter_gallery_ios__start_up:
# description: >
......@@ -186,14 +188,12 @@ tasks:
iOS.
stage: devicelab_ios
required_agent_capabilities: ["has-ios-device"]
flaky: true
basic_material_app_ios__size:
description: >
Measures the IPA size of a basic material app.
stage: devicelab_ios
required_agent_capabilities: ["has-ios-device"]
flaky: true
# microbenchmarks_ios:
# description: >
......@@ -215,14 +215,12 @@ tasks:
Runs end-to-end Flutter tests on iOS.
stage: devicelab_ios
required_agent_capabilities: ["has-ios-device"]
flaky: true
ios_sample_catalog_generator:
description: >
Builds sample catalog markdown pages and iOS screenshots
stage: devicelab_ios
required_agent_capabilities: ["has-ios-device"]
flaky: true
# Tests running on Windows host
......
// 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 'package:flutter_driver/driver_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
String log = '';
void main() {
enableFlutterDriverExtension(handler: (String message) async {
log = 'log:';
await WidgetsBinding.instance.reassembleApplication();
return log;
});
runApp(new MaterialApp(home: const Test()));
}
class Test extends SingleChildRenderObjectWidget {
const Test({ Key key }) : super(key: key);
@override
RenderTest createRenderObject(BuildContext context) {
return new RenderTest();
}
}
class RenderTest extends RenderProxyBox {
RenderTest({ RenderBox child }) : super(child);
@override
void debugPaintSize(PaintingContext context, Offset offset) {
super.debugPaintSize(context, offset);
log += ' debugPaintSize';
}
@override
void paint(PaintingContext context, Offset offset) {
log += ' paint';
}
}
// 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 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
void main() {
FlutterDriver driver;
setUpAll(() async {
driver = await FlutterDriver.connect();
});
tearDownAll(() async {
driver?.close();
});
test('check that we are painting in debugPaintSize mode', () async {
expect(await driver.requestData('status'), 'log: paint debugPaintSize');
});
}
// 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 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
void main() {
FlutterDriver driver;
setUpAll(() async {
driver = await FlutterDriver.connect();
});
tearDownAll(() async {
driver?.close();
});
test('check that we are in normal mode', () async {
expect(await driver.requestData('status'), 'log: paint');
await driver.waitForAbsent(find.byType('PerformanceOverlay'), timeout: Duration.ZERO);
});
}
// 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 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
void main() {
FlutterDriver driver;
setUpAll(() async {
driver = await FlutterDriver.connect();
});
tearDownAll(() async {
driver?.close();
});
test('check that we are showing the performance overlay', () async {
await driver.requestData('status'); // force a reassemble
await driver.waitFor(find.byType('PerformanceOverlay'), timeout: Duration.ZERO);
});
}
// 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 'package:integration_ui/keys.dart' as keys;
......
// 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 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
......
......@@ -348,6 +348,12 @@ class FlutterDriver {
return null;
}
/// Waits until [finder] can no longer locate the target.
Future<Null> waitForAbsent(SerializableFinder finder, {Duration timeout}) async {
await _sendCommand(new WaitForAbsent(finder, timeout: timeout));
return null;
}
/// Waits until there are no more transient callbacks in the queue.
///
/// Use this method when you need to wait for the moment when the application
......@@ -597,9 +603,12 @@ class CommonFinders {
/// Finds [Text] widgets containing string equal to [text].
SerializableFinder text(String text) => new ByText(text);
/// Finds widgets by [key].
/// Finds widgets by [key]. Only [String] and [int] values can be used.
SerializableFinder byValueKey(dynamic key) => new ByValueKey(key);
/// Finds widgets with a tooltip with the given [message].
SerializableFinder byTooltip(String message) => new ByTooltipMessage(message);
/// Finds widgets whose class name matches the given string.
SerializableFinder byType(String type) => new ByType(type);
}
......@@ -86,6 +86,7 @@ class FlutterDriverExtension {
'set_semantics': _setSemantics,
'tap': _tap,
'waitFor': _waitFor,
'waitForAbsent': _waitForAbsent,
'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks,
});
......@@ -100,6 +101,7 @@ class FlutterDriverExtension {
'set_semantics': (Map<String, String> params) => new SetSemantics.deserialize(params),
'tap': (Map<String, String> params) => new Tap.deserialize(params),
'waitFor': (Map<String, String> params) => new WaitFor.deserialize(params),
'waitForAbsent': (Map<String, String> params) => new WaitForAbsent.deserialize(params),
'waitUntilNoTransientCallbacks': (Map<String, String> params) => new WaitUntilNoTransientCallbacks.deserialize(params),
});
......@@ -107,6 +109,7 @@ class FlutterDriverExtension {
'ByText': _createByTextFinder,
'ByTooltipMessage': _createByTooltipMessageFinder,
'ByValueKey': _createByValueKeyFinder,
'ByType': _createByTypeFinder,
});
}
......@@ -195,6 +198,19 @@ class FlutterDriverExtension {
return finder;
}
/// Runs `finder` repeatedly until it finds zero [Element]s.
Future<Finder> _waitForAbsentElement(Finder finder) async {
if (_frameSync)
await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);
await _waitUntilFrame(() => !finder.precache());
if (_frameSync)
await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);
return finder;
}
Finder _createByTextFinder(ByText arguments) {
return find.text(arguments.text);
}
......@@ -219,6 +235,12 @@ class FlutterDriverExtension {
}
}
Finder _createByTypeFinder(ByType arguments) {
return find.byElementPredicate((Element element) {
return element.widget.runtimeType.toString() == arguments.type;
}, description: 'widget with runtimeType "${arguments.type}"');
}
Finder _createFinder(SerializableFinder finder) {
final FinderConstructor constructor = _finders[finder.finderType];
......@@ -242,6 +264,12 @@ class FlutterDriverExtension {
return null;
}
Future<WaitForAbsentResult> _waitForAbsent(Command command) async {
final WaitForAbsent waitForAbsentCommand = command;
await _waitForAbsentElement(_createFinder(waitForAbsentCommand.finder));
return new WaitForAbsentResult();
}
Future<Null> _waitUntilNoTransientCallbacks(Command command) async {
if (SchedulerBinding.instance.transientCallbackCount != 0)
await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);
......
......@@ -62,6 +62,22 @@ class WaitFor extends CommandWithTarget {
WaitFor.deserialize(Map<String, String> json) : super.deserialize(json);
}
/// Waits until [finder] can no longer locate the target.
class WaitForAbsent extends CommandWithTarget {
@override
final String kind = 'waitForAbsent';
/// Creates a command that waits for the widget identified by [finder] to
/// disappear within the [timeout] amount of time.
///
/// If [timeout] is not specified the command times out after 5 seconds.
WaitForAbsent(SerializableFinder finder, {Duration timeout})
: super(finder, timeout: timeout);
/// Deserializes the command from JSON generated by [serialize].
WaitForAbsent.deserialize(Map<String, String> json) : super.deserialize(json);
}
/// Waits until there are no more transient callbacks in the queue.
class WaitUntilNoTransientCallbacks extends Command {
@override
......@@ -85,6 +101,17 @@ class WaitForResult extends Result {
Map<String, dynamic> toJson() => <String, dynamic>{};
}
/// The result of a [WaitForAbsent] command.
class WaitForAbsentResult extends Result {
/// Deserializes the result from JSON.
static WaitForAbsentResult fromJson(Map<String, dynamic> json) {
return new WaitForAbsentResult();
}
@override
Map<String, dynamic> toJson() => <String, dynamic>{};
}
/// Describes how to the driver should search for elements.
abstract class SerializableFinder {
/// Identifies the type of finder to be used by the driver extension.
......@@ -94,6 +121,7 @@ abstract class SerializableFinder {
static SerializableFinder deserialize(Map<String, String> json) {
final String finderType = json['finderType'];
switch(finderType) {
case 'ByType': return ByType.deserialize(json);
case 'ByValueKey': return ByValueKey.deserialize(json);
case 'ByTooltipMessage': return ByTooltipMessage.deserialize(json);
case 'ByText': return ByText.deserialize(json);
......@@ -200,6 +228,28 @@ class ByValueKey extends SerializableFinder {
}
}
/// Finds widgets by their [runtimeType].
class ByType extends SerializableFinder {
@override
final String finderType = 'ByType';
/// Creates a finder that given the runtime type in string form.
ByType(this.type);
/// The widget's [runtimeType], in string form.
final String type;
@override
Map<String, String> serialize() => super.serialize()..addAll(<String, String>{
'type': type,
});
/// Deserializes the finder from JSON generated by [serialize].
static ByType deserialize(Map<String, String> json) {
return new ByType(json['type']);
}
}
/// Command to read the text from a given element.
class GetText extends CommandWithTarget {
@override
......
......@@ -60,6 +60,18 @@ class DriveCommand extends RunCommandBase {
valueHelp:
'url'
);
argParser.addOption(
'driver',
help:
'The test file to run on the host (as opposed to the target file to run on\n'
'the device). By default, this file has the same base name as the target\n'
'file, but in the "test_driver/" directory instead, and with "_test" inserted\n'
'just before the extension, so e.g. if the target is "lib/main.dart", the\n'
'driver will be "test_driver/main_test.dart".',
valueHelp:
'path'
);
}
@override
......@@ -139,6 +151,11 @@ class DriveCommand extends RunCommandBase {
}
String _getTestFile() {
if (argResults['driver'] != null)
return argResults['driver'];
// If the --driver argument wasn't provided, then derive the value from
// the target file.
String appFile = fs.path.normalize(targetFile);
// This command extends `flutter run` and therefore CWD == package dir
......
......@@ -171,6 +171,11 @@ class FlutterDevice {
await view.uiIsolate.flutterToggleDebugPaintSizeEnabled();
}
Future<Null> debugTogglePerformanceOverlayOverride() async {
for (FlutterView view in views)
await view.uiIsolate.flutterTogglePerformanceOverlayOverride();
}
Future<String> togglePlatform({ String from }) async {
String to;
switch (from) {
......@@ -451,6 +456,12 @@ abstract class ResidentRunner {
await device.toggleDebugPaintSizeEnabled();
}
Future<Null> _debugTogglePerformanceOverlayOverride() async {
await refreshViews();
for (FlutterDevice device in flutterDevices)
await device.debugTogglePerformanceOverlayOverride();
}
Future<Null> _screenshot(FlutterDevice device) async {
final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...');
final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png');
......@@ -606,11 +617,16 @@ abstract class ResidentRunner {
await _debugDumpSemanticsTree();
return true;
}
} else if (lower == 'p') {
} else if (character == 'p') {
if (supportsServiceProtocol && isRunningDebug) {
await _debugToggleDebugPaintSizeEnabled();
return true;
}
} else if (character == 'P') {
if (supportsServiceProtocol) {
await _debugTogglePerformanceOverlayOverride();
return true;
}
} else if (character == 's') {
for (FlutterDevice device in flutterDevices) {
if (device.device.supportsScreenshot)
......@@ -726,11 +742,14 @@ abstract class ResidentRunner {
if (supportsServiceProtocol) {
printStatus('You can dump the widget hierarchy of the app (debugDumpApp) by pressing "w".');
printStatus('To dump the rendering tree of the app (debugDumpRenderTree), press "t".');
printStatus('For layers (debugDumpLayerTree), use "L"; accessibility (debugDumpSemantics), "S".');
if (isRunningDebug) {
printStatus('For layers (debugDumpLayerTree), use "L"; accessibility (debugDumpSemantics), "S".');
printStatus('To toggle the display of construction lines (debugPaintSizeEnabled), press "p".');
printStatus('To simulate different operating systems, (defaultTargetPlatform), press "o".');
} else {
printStatus('To dump the accessibility tree (debugDumpSemantics), press "S".');
}
printStatus('To display the performance overlay (WidgetsApp.showPerformanceOverlay), press "P".');
}
if (flutterDevices.any((FlutterDevice d) => d.device.supportsScreenshot))
printStatus('To save a screenshot to flutter.png, press "s".');
......
......@@ -79,7 +79,10 @@ abstract class FlutterCommand extends Command<Null> {
argParser.addOption('target',
abbr: 't',
defaultsTo: flx.defaultMainPath,
help: 'Target app path / main entry-point file.');
help: 'The main entry-point file of the application, as run on the device.\n'
'If the --target option is omitted, but a file name is provided on\n'
'the command line, then that is used instead.',
valueHelp: 'path');
_usesTargetOption = true;
}
......
......@@ -1069,11 +1069,11 @@ class Isolate extends ServiceObjectOwner {
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTree', timeout: kLongRequestTimeout);
}
Future<Map<String, dynamic>> flutterToggleDebugPaintSizeEnabled() async {
Map<String, dynamic> state = await invokeFlutterExtensionRpcRaw('ext.flutter.debugPaint');
Future<Map<String, dynamic>> _flutterToggle(String name) async {
Map<String, dynamic> state = await invokeFlutterExtensionRpcRaw('ext.flutter.$name');
if (state != null && state.containsKey('enabled') && state['enabled'] is String) {
state = await invokeFlutterExtensionRpcRaw(
'ext.flutter.debugPaint',
'ext.flutter.$name',
params: <String, dynamic>{ 'enabled': state['enabled'] == 'true' ? 'false' : 'true' },
timeout: const Duration(milliseconds: 150),
timeoutFatal: false,
......@@ -1082,6 +1082,10 @@ class Isolate extends ServiceObjectOwner {
return state;
}
Future<Map<String, dynamic>> flutterToggleDebugPaintSizeEnabled() => _flutterToggle('debugPaint');
Future<Map<String, dynamic>> flutterTogglePerformanceOverlayOverride() => _flutterToggle('showPerformanceOverlay');
Future<Null> flutterDebugAllowBanner(bool show) async {
await invokeFlutterExtensionRpcRaw(
'ext.flutter.debugAllowBanner',
......
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