Reland "Added --dart-flags option to flutter run (#33924)" (#34181)

Reland "Added --dart-flags option to flutter run (#33924)"

This reverts commit 587687eedf81aa83521cca96b8fb65a4adc8dd90.
diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index c9c5c71..727e25b 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -491,6 +491,8 @@
         cmd.addAll(<String>['--ez', 'start-paused', 'true']);
       if (debuggingOptions.disableServiceAuthCodes)
         cmd.addAll(<String>['--ez', 'disable-service-auth-codes', 'true']);
+      if (debuggingOptions.dartFlags.isNotEmpty)
+        cmd.addAll(<String>['--es', 'dart-flags', debuggingOptions.dartFlags]);
       if (debuggingOptions.useTestFonts)
         cmd.addAll(<String>['--ez', 'use-test-fonts', 'true']);
       if (debuggingOptions.verboseSystemLogs) {
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 1763364..3fb4bf8 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -4,6 +4,8 @@
 
 import 'dart:async';
 
+import 'package:args/command_runner.dart';
+
 import '../base/common.dart';
 import '../base/file_system.dart';
 import '../base/time.dart';
@@ -130,6 +132,16 @@
         defaultsTo: true,
         help: 'If necessary, build the app before running.',
       )
+      ..addOption('dart-flags',
+        hide: !verboseHelp,
+        help: 'Pass a list of comma separated flags to the Dart instance at '
+              'application startup. Flags passed through this option must be '
+              'present on the whitelist defined within the Flutter engine. If '
+              'a non-whitelisted flag is encountered, the process will be '
+              'terminated immediately.\n\n'
+              'This flag is not available on the stable channel and is only '
+              'applied in debug and profile modes. This option should only '
+              'be used for experiments and should not be used by typical users.')
       ..addOption('use-application-binary',
         hide: !verboseHelp,
         help: 'Specify a pre-built application binary to use when running.',
@@ -294,6 +306,7 @@
         buildInfo,
         startPaused: argResults['start-paused'],
         disableServiceAuthCodes: argResults['disable-service-auth-codes'],
+        dartFlags: argResults['dart-flags'] ?? '',
         useTestFonts: argResults['use-test-fonts'],
         enableSoftwareRendering: argResults['enable-software-rendering'],
         skiaDeterministicRendering: argResults['skia-deterministic-rendering'],
@@ -350,6 +363,11 @@
       );
     }
 
+    if (argResults['dart-flags'] != null && FlutterVersion.instance.isStable) {
+      throw UsageException('--dart-flags is not available on the stable '
+                           'channel.', null);
+    }
+
     for (Device device in devices) {
       if (await device.isLocalEmulator) {
         if (await device.supportsHardwareRendering) {
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 394befb..3947ce3 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -377,6 +377,7 @@
     this.buildInfo, {
     this.startPaused = false,
     this.disableServiceAuthCodes = false,
+    this.dartFlags = '',
     this.enableSoftwareRendering = false,
     this.skiaDeterministicRendering = false,
     this.traceSkia = false,
@@ -391,6 +392,7 @@
     : debuggingEnabled = false,
       useTestFonts = false,
       startPaused = false,
+      dartFlags = '',
       disableServiceAuthCodes = false,
       enableSoftwareRendering = false,
       skiaDeterministicRendering = false,
@@ -404,6 +406,7 @@
 
   final BuildInfo buildInfo;
   final bool startPaused;
+  final String dartFlags;
   final bool disableServiceAuthCodes;
   final bool enableSoftwareRendering;
   final bool skiaDeterministicRendering;
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index 02ede71..15f2a2e 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -283,6 +283,11 @@
     if (debuggingOptions.disableServiceAuthCodes)
       launchArguments.add('--disable-service-auth-codes');
 
+    if (debuggingOptions.dartFlags.isNotEmpty) {
+      final String dartFlags = debuggingOptions.dartFlags;
+      launchArguments.add('--dart-flags="$dartFlags"');
+    }
+
     if (debuggingOptions.useTestFonts)
       launchArguments.add('--use-test-fonts');
 
diff --git a/packages/flutter_tools/test/commands/run_test.dart b/packages/flutter_tools/test/commands/run_test.dart
index 492c149..b425238 100644
--- a/packages/flutter_tools/test/commands/run_test.dart
+++ b/packages/flutter_tools/test/commands/run_test.dart
@@ -2,11 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:args/command_runner.dart';
+import 'package:flutter_tools/src/application_package.dart';
 import 'package:flutter_tools/src/base/common.dart';
 import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/run.dart';
 import 'package:flutter_tools/src/device.dart';
 import 'package:flutter_tools/src/runner/flutter_command.dart';
+import 'package:flutter_tools/src/version.dart';
 import 'package:mockito/mockito.dart';
 
 import '../src/common.dart';
@@ -14,9 +18,20 @@
 import '../src/mocks.dart';
 
 void main() {
-  final MockDeviceManager mockDeviceManager = MockDeviceManager();
-
   group('run', () {
+    MockApplicationPackageFactory mockApplicationPackageFactory;
+    MockDeviceManager mockDeviceManager;
+    MockFlutterVersion mockStableFlutterVersion;
+    MockFlutterVersion mockUnstableFlutterVersion;
+
+    setUpAll(() {
+      Cache.disableLocking();
+      mockApplicationPackageFactory = MockApplicationPackageFactory();
+      mockDeviceManager = MockDeviceManager();
+      mockStableFlutterVersion = MockFlutterVersion(isStable: true);
+      mockUnstableFlutterVersion = MockFlutterVersion(isStable: false);
+    });
+
     testUsingContext('fails when target not found', () async {
       final RunCommand command = RunCommand();
       applyMocksToCommand(command);
@@ -28,6 +43,92 @@
       }
     });
 
+
+    group('dart-flags option', () {
+      setUpAll(() {
+        when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) {
+          return Stream<Device>.fromIterable(<Device>[
+            FakeDevice(),
+          ]);
+        });
+      });
+
+      RunCommand command;
+      List<String> args;
+      setUp(() {
+        command = TestRunCommand();
+        args = <String> [
+          'run',
+          '--dart-flags', '"--observe"',
+          '--no-hot',
+        ];
+      });
+
+      testUsingContext('is not available on stable channel', () async {
+        // Stable branch.
+        try {
+          await createTestCommandRunner(command).run(args);
+          fail('Expect exception');
+        // ignore: unused_catch_clause
+        } on UsageException catch(e) {
+          // Not available while on stable branch.
+        }
+      }, overrides: <Type, Generator>{
+        DeviceManager: () => mockDeviceManager,
+        FlutterVersion: () => mockStableFlutterVersion,
+      });
+
+      testUsingContext('is populated in debug mode', () async {
+        // FakeDevice.startApp checks that --dart-flags doesn't get dropped and
+        // throws ToolExit with FakeDevice.kSuccess if the flag is populated.
+        try {
+          await createTestCommandRunner(command).run(args);
+          fail('Expect exception');
+        } on ToolExit catch (e) {
+          expect(e.exitCode, FakeDevice.kSuccess);
+        }
+      }, overrides: <Type, Generator>{
+        ApplicationPackageFactory: () => mockApplicationPackageFactory,
+        DeviceManager: () => mockDeviceManager,
+        FlutterVersion: () => mockUnstableFlutterVersion,
+      });
+
+      testUsingContext('is populated in profile mode', () async {
+        args.add('--profile');
+
+        // FakeDevice.startApp checks that --dart-flags doesn't get dropped and
+        // throws ToolExit with FakeDevice.kSuccess if the flag is populated.
+        try {
+          await createTestCommandRunner(command).run(args);
+          fail('Expect exception');
+        } on ToolExit catch (e) {
+          expect(e.exitCode, FakeDevice.kSuccess);
+        }
+      }, overrides: <Type, Generator>{
+        ApplicationPackageFactory: () => mockApplicationPackageFactory,
+        DeviceManager: () => mockDeviceManager,
+        FlutterVersion: () => mockUnstableFlutterVersion,
+      });
+
+      testUsingContext('is not populated in release mode', () async {
+        args.add('--release');
+
+        // FakeDevice.startApp checks that --dart-flags *does* get dropped and
+        // throws ToolExit with FakeDevice.kSuccess if the flag is set to the
+        // empty string.
+        try {
+          await createTestCommandRunner(command).run(args);
+          fail('Expect exception');
+        } on ToolExit catch (e) {
+          expect(e.exitCode, FakeDevice.kSuccess);
+        }
+      }, overrides: <Type, Generator>{
+        ApplicationPackageFactory: () => mockApplicationPackageFactory,
+        DeviceManager: () => mockDeviceManager,
+        FlutterVersion: () => mockUnstableFlutterVersion,
+      });
+    });
+
     testUsingContext('should only request artifacts corresponding to connected devices', () async {
       when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) {
         return Stream<Device>.fromIterable(<Device>[
@@ -89,3 +190,73 @@
   @override
   Future<TargetPlatform> get targetPlatform async => _targetPlatform;
 }
+
+class TestRunCommand extends RunCommand {
+  @override
+  // ignore: must_call_super
+  Future<void> validateCommand() async {
+    devices = await deviceManager.getDevices().toList();
+  }
+}
+
+class MockStableFlutterVersion extends MockFlutterVersion {
+  @override
+  bool get isStable => true;
+}
+
+class FakeDevice extends Fake implements Device {
+  static const int kSuccess = 1;
+  static const int kFailure = -1;
+  final TargetPlatform _targetPlatform = TargetPlatform.ios;
+
+  void _throwToolExit(int code) => throwToolExit(null, exitCode: code);
+
+  @override
+  Future<bool> get isLocalEmulator => Future<bool>.value(false);
+
+  @override
+  bool get supportsHotReload => false;
+
+  @override
+  Future<String> get sdkNameAndVersion => Future<String>.value('');
+
+  @override
+  DeviceLogReader getLogReader({ ApplicationPackage app }) {
+    return MockDeviceLogReader();
+  }
+
+  @override
+  String get name => 'FakeDevice';
+
+  @override
+  Future<TargetPlatform> get targetPlatform async => _targetPlatform;
+
+  @override
+  Future<LaunchResult> startApp(
+    ApplicationPackage package, {
+    String mainPath,
+    String route,
+    DebuggingOptions debuggingOptions,
+    Map<String, dynamic> platformArgs,
+    bool prebuiltApplication = false,
+    bool usesTerminalUi = true,
+    bool ipv6 = false,
+  }) async {
+    final String dartFlags = debuggingOptions.dartFlags;
+    // In release mode, --dart-flags should be set to the empty string and
+    // provided flags should be dropped. In debug and profile modes,
+    // --dart-flags should not be empty.
+    if (debuggingOptions.buildInfo.isRelease) {
+      if (dartFlags.isNotEmpty) {
+        _throwToolExit(kFailure);
+      }
+      _throwToolExit(kSuccess);
+    } else {
+      if (dartFlags.isEmpty) {
+        _throwToolExit(kFailure);
+      }
+      _throwToolExit(kSuccess);
+    }
+    return null;
+  }
+}
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index 263437e..cc145f3 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -332,8 +332,12 @@
 }
 
 class MockFlutterVersion extends Mock implements FlutterVersion {
+  MockFlutterVersion({bool isStable = false}) : _isStable = isStable;
+
+  final bool _isStable;
+
   @override
-  bool get isStable => false;
+  bool get isStable => _isStable;
 }
 
 class MockClock extends Mock implements SystemClock {}
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index 5e93576..aad8665 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -37,6 +37,18 @@
   );
 }
 
+class MockApplicationPackageFactory extends Mock implements ApplicationPackageFactory {
+  final MockApplicationPackageStore _store = MockApplicationPackageStore();
+
+  @override
+  Future<ApplicationPackage> getPackageForPlatform(
+    TargetPlatform platform, {
+    File applicationBinary,
+  }) async {
+    return _store.getPackageForPlatform(platform);
+  }
+}
+
 /// An SDK installation with several SDK levels (19, 22, 23).
 class MockAndroidSdk extends Mock implements AndroidSdk {
   static Directory createSdkDirectory({