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

diff --git a/packages/flutter/lib/src/foundation/binding.dart b/packages/flutter/lib/src/foundation/binding.dart
index 2e070ed..617d707 100644
--- a/packages/flutter/lib/src/foundation/binding.dart
+++ b/packages/flutter/lib/src/foundation/binding.dart
@@ -11,6 +11,7 @@
 
 import 'assertions.dart';
 import 'basic_types.dart';
+import 'platform.dart';
 
 /// Signature for service extensions.
 ///
@@ -115,6 +116,36 @@
       name: 'frameworkPresent',
       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; });
   }
 
@@ -133,7 +164,6 @@
     return new Future<Null>.value();
   }
 
-
   /// Registers a service extension method with the given name (full
   /// name "ext.flutter.name"), which takes no arguments and returns
   /// no value.
diff --git a/packages/flutter/lib/src/foundation/debug.dart b/packages/flutter/lib/src/foundation/debug.dart
index 8adfc3d..02a8787 100644
--- a/packages/flutter/lib/src/foundation/debug.dart
+++ b/packages/flutter/lib/src/foundation/debug.dart
@@ -4,6 +4,7 @@
 
 import 'assertions.dart';
 import 'print.dart';
+import 'platform.dart';
 
 /// Returns true if none of the foundation library debug variables have been
 /// changed.
@@ -20,7 +21,8 @@
 /// a complete list.
 bool debugAssertAllFoundationVarsUnset(String reason, { DebugPrintCallback debugPrintOverride: debugPrintThrottled }) {
   assert(() {
-    if (debugPrint != debugPrintOverride)
+    if (debugPrint != debugPrintOverride ||
+        debugDefaultTargetPlatformOverride != null)
       throw new FlutterError(reason);
     return true;
   });
diff --git a/packages/flutter/lib/src/foundation/platform.dart b/packages/flutter/lib/src/foundation/platform.dart
index 98224ba..4e86b25 100644
--- a/packages/flutter/lib/src/foundation/platform.dart
+++ b/packages/flutter/lib/src/foundation/platform.dart
@@ -28,19 +28,22 @@
 /// originally written assuming Android-like behavior, and we added platform
 /// adaptations for iOS later). Tests can check iOS behavior by using the
 /// 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 result;
   if (Platform.isIOS || Platform.isMacOS) {
     result = TargetPlatform.iOS;
   } else if (Platform.isAndroid || Platform.isLinux) {
     result = TargetPlatform.android;
-  } else if (Platform.operatingSystem == "fuchsia") {
+  } else if (Platform.operatingSystem == 'fuchsia') {
     result = TargetPlatform.fuchsia;
   }
   assert(() {
     if (Platform.environment.containsKey('FLUTTER_TEST'))
       result = TargetPlatform.android;
+    if (debugDefaultTargetPlatformOverride != null)
+      result = debugDefaultTargetPlatformOverride;
     return true;
   });
   if (result == null) {
@@ -52,3 +55,12 @@
   }
   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;
diff --git a/packages/flutter/test/foundation/service_extensions_test.dart b/packages/flutter/test/foundation/service_extensions_test.dart
index 3d02b09..9579398 100644
--- a/packages/flutter/test/foundation/service_extensions_test.dart
+++ b/packages/flutter/test/foundation/service_extensions_test.dart
@@ -65,8 +65,23 @@
   }
 }
 
+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() {
-  TestServiceExtensionsBinding binding;
   List<String> console = <String>[];
 
   test('Service extensions - pretest', () async {
@@ -216,6 +231,41 @@
     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 {
     Map<String, String> result;
     Future<Map<String, String>> pendingResult;
@@ -329,7 +379,7 @@
   test('Service extensions - posttest', () async {
     // If you add a service extension... TEST IT! :-)
     // ...then increment this number.
-    expect(binding.extensions.length, 11);
+    expect(binding.extensions.length, 12);
 
     expect(console, isEmpty);
     debugPrint = debugPrintThrottled;
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index d207ffd..2450bd5 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -111,24 +111,26 @@
     Status status = logger.startProgress('Taking screenshot...');
     File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png');
     try {
-      if (vmService != null)
-        await vmService.vm.refreshViews();
-      try {
-        if (isRunningDebug)
+      if (supportsServiceProtocol && isRunningDebug) {
+        if (vmService != null)
+          await vmService.vm.refreshViews();
+        try {
           await currentView.uiIsolate.flutterDebugAllowBanner(false);
-      } catch (error) {
-        status.stop();
-        printError(error);
+        } catch (error) {
+          status.stop();
+          printError(error);
+        }
       }
       try {
         await device.takeScreenshot(outputFile);
       } finally {
-        try {
-          if (isRunningDebug)
+        if (supportsServiceProtocol && isRunningDebug) {
+          try {
             await currentView.uiIsolate.flutterDebugAllowBanner(true);
-        } catch (error) {
-          status.stop();
-          printError(error);
+          } catch (error) {
+            status.stop();
+            printError(error);
+          }
         }
       }
       int sizeKB = (await outputFile.length()) ~/ 1024;
@@ -140,6 +142,18 @@
     }
   }
 
+  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() {
     assert(stayResident);
     ProcessSignal.SIGINT.watch().listen(_cleanUpAndExit);
@@ -228,25 +242,31 @@
       printHelp(details: true);
       return true;
     } else if (lower == 'w') {
-      if (!supportsServiceProtocol)
+      if (supportsServiceProtocol) {
+        await _debugDumpApp();
         return true;
-      await _debugDumpApp();
-      return true;
+      }
     } else if (lower == 't') {
-      if (!supportsServiceProtocol)
+      if (supportsServiceProtocol) {
+        await _debugDumpRenderTree();
         return true;
-      await _debugDumpRenderTree();
-      return true;
+      }
     } else if (lower == 'p') {
-      if (!supportsServiceProtocol)
+      if (supportsServiceProtocol && isRunningDebug) {
+        await _debugToggleDebugPaintSizeEnabled();
         return true;
-      await _debugToggleDebugPaintSizeEnabled();
-      return true;
+      }
     } else if (lower == 's') {
-      if (!supportsServiceProtocol || !device.supportsScreenshot)
+      if (device.supportsScreenshot) {
+        await _screenshot();
         return true;
-      await _screenshot();
-      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) {
       // F10, exit
       await stop();
@@ -347,7 +367,10 @@
     if (supportsServiceProtocol) {
       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 toggle the display of construction lines (debugPaintSizeEnabled), press "p".');
+      if (isRunningDebug) {
+        printStatus('To toggle the display of construction lines (debugPaintSizeEnabled), press "p".');
+        printStatus('To simulate different operating systems, (defaultTargetPlatform), press "o".');
+      }
     }
     if (device.supportsScreenshot)
       printStatus('To save a screenshot to flutter.png, press "s".');
diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart
index 95a9bb6..b3114d8 100644
--- a/packages/flutter_tools/lib/src/vmservice.dart
+++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -924,6 +924,18 @@
       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> {