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

Introduce a quick way to test across platforms (#8262)

parent c233f382
...@@ -54,7 +54,7 @@ class GalleryAppState extends State<GalleryApp> { ...@@ -54,7 +54,7 @@ class GalleryAppState extends State<GalleryApp> {
bool _showPerformanceOverlay = false; bool _showPerformanceOverlay = false;
bool _checkerboardRasterCacheImages = false; bool _checkerboardRasterCacheImages = false;
double _timeDilation = 1.0; double _timeDilation = 1.0;
TargetPlatform _platform = defaultTargetPlatform; TargetPlatform _platform;
Timer _timeDilationTimer; Timer _timeDilationTimer;
...@@ -94,7 +94,7 @@ class GalleryAppState extends State<GalleryApp> { ...@@ -94,7 +94,7 @@ class GalleryAppState extends State<GalleryApp> {
} : null, } : null,
onPlatformChanged: (TargetPlatform value) { onPlatformChanged: (TargetPlatform value) {
setState(() { setState(() {
_platform = value; _platform = value == defaultTargetPlatform ? null : value;
}); });
}, },
timeDilation: _timeDilation, timeDilation: _timeDilation,
...@@ -128,7 +128,7 @@ class GalleryAppState extends State<GalleryApp> { ...@@ -128,7 +128,7 @@ class GalleryAppState extends State<GalleryApp> {
return new MaterialApp( return new MaterialApp(
title: 'Flutter Gallery', title: 'Flutter Gallery',
color: Colors.grey[500], color: Colors.grey[500],
theme: (_useLightTheme ? _kGalleryLightTheme : _kGalleryDarkTheme).copyWith(platform: _platform), theme: (_useLightTheme ? _kGalleryLightTheme : _kGalleryDarkTheme).copyWith(platform: _platform ?? defaultTargetPlatform),
showPerformanceOverlay: _showPerformanceOverlay, showPerformanceOverlay: _showPerformanceOverlay,
checkerboardRasterCacheImages: _checkerboardRasterCacheImages, checkerboardRasterCacheImages: _checkerboardRasterCacheImages,
routes: _kRoutes, routes: _kRoutes,
......
...@@ -11,6 +11,7 @@ import 'package:meta/meta.dart'; ...@@ -11,6 +11,7 @@ import 'package:meta/meta.dart';
import 'assertions.dart'; import 'assertions.dart';
import 'basic_types.dart'; import 'basic_types.dart';
import 'platform.dart';
/// Signature for service extensions. /// Signature for service extensions.
/// ///
...@@ -115,6 +116,36 @@ abstract class BindingBase { ...@@ -115,6 +116,36 @@ abstract class BindingBase {
name: 'frameworkPresent', name: 'frameworkPresent',
callback: () => new Future<Null>.value() callback: () => new Future<Null>.value()
); );
assert(() {
registerServiceExtension(
name: 'platformOverride',
callback: (Map<String, String> parameters) async {
if (parameters.containsKey('value')) {
switch (parameters['value']) {
case 'android':
debugDefaultTargetPlatformOverride = TargetPlatform.android;
break;
case 'iOS':
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
break;
case 'fuchsia':
debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;
break;
case 'default':
default:
debugDefaultTargetPlatformOverride = null;
}
await reassembleApplication();
}
return <String, String>{
'value': defaultTargetPlatform
.toString()
.substring('$TargetPlatform.'.length),
};
}
);
return true;
});
assert(() { _debugServiceExtensionsRegistered = true; return true; }); assert(() { _debugServiceExtensionsRegistered = true; return true; });
} }
...@@ -133,7 +164,6 @@ abstract class BindingBase { ...@@ -133,7 +164,6 @@ abstract class BindingBase {
return new Future<Null>.value(); return new Future<Null>.value();
} }
/// Registers a service extension method with the given name (full /// Registers a service extension method with the given name (full
/// name "ext.flutter.name"), which takes no arguments and returns /// name "ext.flutter.name"), which takes no arguments and returns
/// no value. /// no value.
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'assertions.dart'; import 'assertions.dart';
import 'print.dart'; import 'print.dart';
import 'platform.dart';
/// Returns true if none of the foundation library debug variables have been /// Returns true if none of the foundation library debug variables have been
/// changed. /// changed.
...@@ -20,7 +21,8 @@ import 'print.dart'; ...@@ -20,7 +21,8 @@ import 'print.dart';
/// a complete list. /// a complete list.
bool debugAssertAllFoundationVarsUnset(String reason, { DebugPrintCallback debugPrintOverride: debugPrintThrottled }) { bool debugAssertAllFoundationVarsUnset(String reason, { DebugPrintCallback debugPrintOverride: debugPrintThrottled }) {
assert(() { assert(() {
if (debugPrint != debugPrintOverride) if (debugPrint != debugPrintOverride ||
debugDefaultTargetPlatformOverride != null)
throw new FlutterError(reason); throw new FlutterError(reason);
return true; return true;
}); });
......
...@@ -28,19 +28,22 @@ enum TargetPlatform { ...@@ -28,19 +28,22 @@ enum TargetPlatform {
/// originally written assuming Android-like behavior, and we added platform /// originally written assuming Android-like behavior, and we added platform
/// adaptations for iOS later). Tests can check iOS behavior by using the /// adaptations for iOS later). Tests can check iOS behavior by using the
/// platform override APIs (such as [ThemeData.platform] in the material /// platform override APIs (such as [ThemeData.platform] in the material
/// library). /// library) or by calling [debugSetDefaultTargetPlatformOverride]. The value
/// can only be explicitly set in debug mode.
TargetPlatform get defaultTargetPlatform { TargetPlatform get defaultTargetPlatform {
TargetPlatform result; TargetPlatform result;
if (Platform.isIOS || Platform.isMacOS) { if (Platform.isIOS || Platform.isMacOS) {
result = TargetPlatform.iOS; result = TargetPlatform.iOS;
} else if (Platform.isAndroid || Platform.isLinux) { } else if (Platform.isAndroid || Platform.isLinux) {
result = TargetPlatform.android; result = TargetPlatform.android;
} else if (Platform.operatingSystem == "fuchsia") { } else if (Platform.operatingSystem == 'fuchsia') {
result = TargetPlatform.fuchsia; result = TargetPlatform.fuchsia;
} }
assert(() { assert(() {
if (Platform.environment.containsKey('FLUTTER_TEST')) if (Platform.environment.containsKey('FLUTTER_TEST'))
result = TargetPlatform.android; result = TargetPlatform.android;
if (debugDefaultTargetPlatformOverride != null)
result = debugDefaultTargetPlatformOverride;
return true; return true;
}); });
if (result == null) { if (result == null) {
...@@ -52,3 +55,12 @@ TargetPlatform get defaultTargetPlatform { ...@@ -52,3 +55,12 @@ TargetPlatform get defaultTargetPlatform {
} }
return result; return result;
} }
/// Override the [defaultTargetPlatform].
///
/// Setting this to null returns the [defaultTargetPlatform] to its original
/// value (based on the actual current platform).
///
/// This setter is only available intended for debugging purposes. To change the
/// target platform in release builds, use the platform override APIs (such as
/// [ThemeData.platform] in the material library).
TargetPlatform debugDefaultTargetPlatformOverride;
...@@ -65,8 +65,23 @@ class TestServiceExtensionsBinding extends BindingBase ...@@ -65,8 +65,23 @@ class TestServiceExtensionsBinding extends BindingBase
} }
} }
TestServiceExtensionsBinding binding;
Future<Map<String, String>> hasReassemble(Future<Map<String, String>> pendingResult) async {
bool completed = false;
pendingResult.whenComplete(() { completed = true; });
expect(binding.frameScheduled, isFalse);
await binding.flushMicrotasks();
expect(binding.frameScheduled, isTrue);
expect(completed, isFalse);
binding.doFrame();
await binding.flushMicrotasks();
expect(completed, isTrue);
expect(binding.frameScheduled, isFalse);
return pendingResult;
}
void main() { void main() {
TestServiceExtensionsBinding binding;
List<String> console = <String>[]; List<String> console = <String>[];
test('Service extensions - pretest', () async { test('Service extensions - pretest', () async {
...@@ -216,6 +231,41 @@ void main() { ...@@ -216,6 +231,41 @@ void main() {
expect(result, <String, String>{}); expect(result, <String, String>{});
}); });
test('Service extensions - platformOverride', () async {
Map<String, String> result;
expect(binding.reassembled, 0);
expect(defaultTargetPlatform, TargetPlatform.android);
result = await binding.testExtension('platformOverride', <String, String>{});
expect(result, <String, String>{'value': 'android'});
expect(defaultTargetPlatform, TargetPlatform.android);
result = await hasReassemble(binding.testExtension('platformOverride', <String, String>{'value': 'iOS'}));
expect(result, <String, String>{'value': 'iOS'});
expect(binding.reassembled, 1);
expect(defaultTargetPlatform, TargetPlatform.iOS);
result = await hasReassemble(binding.testExtension('platformOverride', <String, String>{'value': 'android'}));
expect(result, <String, String>{'value': 'android'});
expect(binding.reassembled, 2);
expect(defaultTargetPlatform, TargetPlatform.android);
result = await hasReassemble(binding.testExtension('platformOverride', <String, String>{'value': 'fuchsia'}));
expect(result, <String, String>{'value': 'fuchsia'});
expect(binding.reassembled, 3);
expect(defaultTargetPlatform, TargetPlatform.fuchsia);
result = await hasReassemble(binding.testExtension('platformOverride', <String, String>{'value': 'default'}));
expect(result, <String, String>{'value': 'android'});
expect(binding.reassembled, 4);
expect(defaultTargetPlatform, TargetPlatform.android);
result = await hasReassemble(binding.testExtension('platformOverride', <String, String>{'value': 'iOS'}));
expect(result, <String, String>{'value': 'iOS'});
expect(binding.reassembled, 5);
expect(defaultTargetPlatform, TargetPlatform.iOS);
result = await hasReassemble(binding.testExtension('platformOverride', <String, String>{'value': 'bogus'}));
expect(result, <String, String>{'value': 'android'});
expect(binding.reassembled, 6);
expect(defaultTargetPlatform, TargetPlatform.android);
binding.reassembled = 0;
});
test('Service extensions - repaintRainbow', () async { test('Service extensions - repaintRainbow', () async {
Map<String, String> result; Map<String, String> result;
Future<Map<String, String>> pendingResult; Future<Map<String, String>> pendingResult;
...@@ -329,7 +379,7 @@ void main() { ...@@ -329,7 +379,7 @@ void main() {
test('Service extensions - posttest', () async { test('Service extensions - posttest', () async {
// If you add a service extension... TEST IT! :-) // If you add a service extension... TEST IT! :-)
// ...then increment this number. // ...then increment this number.
expect(binding.extensions.length, 11); expect(binding.extensions.length, 12);
expect(console, isEmpty); expect(console, isEmpty);
debugPrint = debugPrintThrottled; debugPrint = debugPrintThrottled;
......
...@@ -111,26 +111,28 @@ abstract class ResidentRunner { ...@@ -111,26 +111,28 @@ abstract class ResidentRunner {
Status status = logger.startProgress('Taking screenshot...'); Status status = logger.startProgress('Taking screenshot...');
File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png'); File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png');
try { try {
if (supportsServiceProtocol && isRunningDebug) {
if (vmService != null) if (vmService != null)
await vmService.vm.refreshViews(); await vmService.vm.refreshViews();
try { try {
if (isRunningDebug)
await currentView.uiIsolate.flutterDebugAllowBanner(false); await currentView.uiIsolate.flutterDebugAllowBanner(false);
} catch (error) { } catch (error) {
status.stop(); status.stop();
printError(error); printError(error);
} }
}
try { try {
await device.takeScreenshot(outputFile); await device.takeScreenshot(outputFile);
} finally { } finally {
if (supportsServiceProtocol && isRunningDebug) {
try { try {
if (isRunningDebug)
await currentView.uiIsolate.flutterDebugAllowBanner(true); await currentView.uiIsolate.flutterDebugAllowBanner(true);
} catch (error) { } catch (error) {
status.stop(); status.stop();
printError(error); printError(error);
} }
} }
}
int sizeKB = (await outputFile.length()) ~/ 1024; int sizeKB = (await outputFile.length()) ~/ 1024;
status.stop(); status.stop();
printStatus('Screenshot written to ${fs.path.relative(outputFile.path)} (${sizeKB}kB).'); printStatus('Screenshot written to ${fs.path.relative(outputFile.path)} (${sizeKB}kB).');
...@@ -140,6 +142,18 @@ abstract class ResidentRunner { ...@@ -140,6 +142,18 @@ abstract class ResidentRunner {
} }
} }
Future<String> _debugRotatePlatform() async {
if (vmService != null)
await vmService.vm.refreshViews();
switch (await currentView.uiIsolate.flutterPlatformOverride()) {
case 'iOS':
return await currentView.uiIsolate.flutterPlatformOverride('android');
case 'android':
default:
return await currentView.uiIsolate.flutterPlatformOverride('iOS');
}
}
void registerSignalHandlers() { void registerSignalHandlers() {
assert(stayResident); assert(stayResident);
ProcessSignal.SIGINT.watch().listen(_cleanUpAndExit); ProcessSignal.SIGINT.watch().listen(_cleanUpAndExit);
...@@ -228,25 +242,31 @@ abstract class ResidentRunner { ...@@ -228,25 +242,31 @@ abstract class ResidentRunner {
printHelp(details: true); printHelp(details: true);
return true; return true;
} else if (lower == 'w') { } else if (lower == 'w') {
if (!supportsServiceProtocol) if (supportsServiceProtocol) {
return true;
await _debugDumpApp(); await _debugDumpApp();
return true; return true;
}
} else if (lower == 't') { } else if (lower == 't') {
if (!supportsServiceProtocol) if (supportsServiceProtocol) {
return true;
await _debugDumpRenderTree(); await _debugDumpRenderTree();
return true; return true;
}
} else if (lower == 'p') { } else if (lower == 'p') {
if (!supportsServiceProtocol) if (supportsServiceProtocol && isRunningDebug) {
return true;
await _debugToggleDebugPaintSizeEnabled(); await _debugToggleDebugPaintSizeEnabled();
return true; return true;
}
} else if (lower == 's') { } else if (lower == 's') {
if (!supportsServiceProtocol || !device.supportsScreenshot) if (device.supportsScreenshot) {
return true;
await _screenshot(); await _screenshot();
return true; return true;
}
} else if (lower == 'o') {
if (supportsServiceProtocol && isRunningDebug) {
String platform = await _debugRotatePlatform();
print('Switched operating system to: $platform');
return true;
}
} else if (lower == 'q' || character == AnsiTerminal.KEY_F10) { } else if (lower == 'q' || character == AnsiTerminal.KEY_F10) {
// F10, exit // F10, exit
await stop(); await stop();
...@@ -347,7 +367,10 @@ abstract class ResidentRunner { ...@@ -347,7 +367,10 @@ abstract class ResidentRunner {
if (supportsServiceProtocol) { if (supportsServiceProtocol) {
printStatus('To dump the widget hierarchy of the app (debugDumpApp), press "w".'); printStatus('To dump the widget hierarchy of the app (debugDumpApp), press "w".');
printStatus('To dump the rendering tree of the app (debugDumpRenderTree), press "t".'); printStatus('To dump the rendering tree of the app (debugDumpRenderTree), press "t".');
if (isRunningDebug) {
printStatus('To toggle the display of construction lines (debugPaintSizeEnabled), press "p".'); printStatus('To toggle the display of construction lines (debugPaintSizeEnabled), press "p".');
printStatus('To simulate different operating systems, (defaultTargetPlatform), press "o".');
}
} }
if (device.supportsScreenshot) if (device.supportsScreenshot)
printStatus('To save a screenshot to flutter.png, press "s".'); printStatus('To save a screenshot to flutter.png, press "s".');
......
...@@ -924,6 +924,18 @@ class Isolate extends ServiceObjectOwner { ...@@ -924,6 +924,18 @@ class Isolate extends ServiceObjectOwner {
timeoutFatal: false, timeoutFatal: false,
); );
} }
Future<Null> flutterPlatformOverride([String platform]) async {
Map<String, String> result = await invokeFlutterExtensionRpcRaw(
'ext.flutter.platformOverride',
params: platform != null ? <String, dynamic>{ 'value': platform } : <String, String>{},
timeout: const Duration(seconds: 5),
timeoutFatal: false,
);
if (result != null && result.containsKey('value') && result['value'] is String)
return result['value'];
return 'unknown';
}
} }
class ServiceMap extends ServiceObject implements Map<String, dynamic> { class ServiceMap extends ServiceObject implements Map<String, dynamic> {
......
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