add an --enable-vmservice flag (#50663)

diff --git a/packages/flutter_tools/bin/fuchsia_tester.dart b/packages/flutter_tools/bin/fuchsia_tester.dart
index 0e60a96..0fc00e5 100644
--- a/packages/flutter_tools/bin/fuchsia_tester.dart
+++ b/packages/flutter_tools/bin/fuchsia_tester.dart
@@ -142,7 +142,8 @@
       tests[source] = dill;
     }
 
-    exitCode = await runTests(
+    // TODO(dnfield): This should be injected.
+    exitCode = await const FlutterTestRunner().runTests(
       const TestWrapper(),
       tests.keys.toList(),
       workDir: testDirectory,
diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart
index 101b142..a4a26f7 100644
--- a/packages/flutter_tools/lib/src/commands/test.dart
+++ b/packages/flutter_tools/lib/src/commands/test.dart
@@ -27,6 +27,7 @@
   TestCommand({
     bool verboseHelp = false,
     this.testWrapper = const TestWrapper(),
+    this.testRunner = const FlutterTestRunner(),
   }) : assert(testWrapper != null) {
     requiresPubspecYaml();
     usesPubOption();
@@ -110,6 +111,15 @@
               'test cases (must be a 32bit unsigned integer).\n'
               'If "random", pick a random seed to use.\n'
               'If 0 or not set, do not randomize test case execution order.',
+      )
+      ..addFlag('enable-vmservice',
+        defaultsTo: false,
+        hide: !verboseHelp,
+        help: 'Enables the vmservice without --start-paused. This flag is '
+              'intended for use with tests that will use dart:developer to '
+              'interact with the vmservice at runtime.\n'
+              'This flag is ignored if --start-paused or coverage are requested. '
+              'The vmservice will be enabled no matter what in those cases.'
       );
     usesTrackWidgetCreation(verboseHelp: verboseHelp);
   }
@@ -117,6 +127,9 @@
   /// The interface for starting and configuring the tester.
   final TestWrapper testWrapper;
 
+  /// Interface for running the tester process.
+  final FlutterTestRunner testRunner;
+
   @override
   Future<Set<DevelopmentArtifact>> get requiredArtifacts async {
     final Set<DevelopmentArtifact> results = <DevelopmentArtifact>{};
@@ -235,14 +248,14 @@
     final bool disableServiceAuthCodes =
       boolArg('disable-service-auth-codes');
 
-    final int result = await runTests(
+    final int result = await testRunner.runTests(
       testWrapper,
       files,
       workDir: workDir,
       names: names,
       plainNames: plainNames,
       watcher: watcher,
-      enableObservatory: collector != null || startPaused,
+      enableObservatory: collector != null || startPaused || boolArg('enable-vmservice'),
       startPaused: startPaused,
       disableServiceAuthCodes: disableServiceAuthCodes,
       ipv6: boolArg('ipv6'),
diff --git a/packages/flutter_tools/lib/src/test/runner.dart b/packages/flutter_tools/lib/src/test/runner.dart
index 1577d1c..6f1b095 100644
--- a/packages/flutter_tools/lib/src/test/runner.dart
+++ b/packages/flutter_tools/lib/src/test/runner.dart
@@ -20,141 +20,177 @@
 import 'test_wrapper.dart';
 import 'watcher.dart';
 
-/// Runs tests using package:test and the Flutter engine.
-Future<int> runTests(
-  TestWrapper testWrapper,
-  List<String> testFiles, {
-  Directory workDir,
-  List<String> names = const <String>[],
-  List<String> plainNames = const <String>[],
-  bool enableObservatory = false,
-  bool startPaused = false,
-  bool disableServiceAuthCodes = false,
-  bool ipv6 = false,
-  bool machine = false,
-  String precompiledDillPath,
-  Map<String, String> precompiledDillFiles,
-  @required BuildMode buildMode,
-  bool trackWidgetCreation = false,
-  bool updateGoldens = false,
-  TestWatcher watcher,
-  @required int concurrency,
-  bool buildTestAssets = false,
-  FlutterProject flutterProject,
-  String icudtlPath,
-  Directory coverageDirectory,
-  bool web = false,
-  String randomSeed = '0',
-}) async {
-  // Configure package:test to use the Flutter engine for child processes.
-  final String shellPath = globals.artifacts.getArtifactPath(Artifact.flutterTester);
-  if (!globals.processManager.canRun(shellPath)) {
-    throwToolExit('Cannot execute Flutter tester at $shellPath');
-  }
+/// A class that abstracts launching the test process from the test runner.
+abstract class FlutterTestRunner {
+  const factory FlutterTestRunner() = _FlutterTestRunnerImpl;
 
-  // Compute the command-line arguments for package:test.
-  final List<String> testArgs = <String>[
-    if (!globals.terminal.supportsColor)
-      '--no-color',
-    if (startPaused)
-      '--pause-after-load',
-    if (machine)
-      ...<String>['-r', 'json']
-    else
-      ...<String>['-r', 'compact'],
-    '--concurrency=$concurrency',
-    for (final String name in names)
-      ...<String>['--name', name],
-    for (final String plainName in plainNames)
-      ...<String>['--plain-name', plainName],
-    '--test-randomize-ordering-seed=$randomSeed',
-  ];
-  if (web) {
-    final String tempBuildDir = globals.fs.systemTempDirectory
-      .createTempSync('flutter_test.')
-      .absolute
-      .uri
-      .toFilePath();
-    final bool result = await webCompilationProxy.initialize(
-      projectDirectory: flutterProject.directory,
-      testOutputDir: tempBuildDir,
-      testFiles: testFiles,
-      projectName: flutterProject.manifest.appName,
-      initializePlatform: true,
-    );
-    if (!result) {
-      throwToolExit('Failed to compile tests');
+  /// Runs tests using package:test and the Flutter engine.
+  Future<int> runTests(
+    TestWrapper testWrapper,
+    List<String> testFiles, {
+    Directory workDir,
+    List<String> names = const <String>[],
+    List<String> plainNames = const <String>[],
+    bool enableObservatory = false,
+    bool startPaused = false,
+    bool disableServiceAuthCodes = false,
+    bool ipv6 = false,
+    bool machine = false,
+    String precompiledDillPath,
+    Map<String, String> precompiledDillFiles,
+    @required BuildMode buildMode,
+    bool trackWidgetCreation = false,
+    bool updateGoldens = false,
+    TestWatcher watcher,
+    @required int concurrency,
+    bool buildTestAssets = false,
+    FlutterProject flutterProject,
+    String icudtlPath,
+    Directory coverageDirectory,
+    bool web = false,
+    String randomSeed = '0',
+  });
+}
+
+class _FlutterTestRunnerImpl implements FlutterTestRunner {
+  const _FlutterTestRunnerImpl();
+
+  @override
+  Future<int> runTests(
+    TestWrapper testWrapper,
+    List<String> testFiles, {
+    Directory workDir,
+    List<String> names = const <String>[],
+    List<String> plainNames = const <String>[],
+    bool enableObservatory = false,
+    bool startPaused = false,
+    bool disableServiceAuthCodes = false,
+    bool ipv6 = false,
+    bool machine = false,
+    String precompiledDillPath,
+    Map<String, String> precompiledDillFiles,
+    @required BuildMode buildMode,
+    bool trackWidgetCreation = false,
+    bool updateGoldens = false,
+    TestWatcher watcher,
+    @required int concurrency,
+    bool buildTestAssets = false,
+    FlutterProject flutterProject,
+    String icudtlPath,
+    Directory coverageDirectory,
+    bool web = false,
+    String randomSeed = '0',
+  }) async {
+    // Configure package:test to use the Flutter engine for child processes.
+    final String shellPath = globals.artifacts.getArtifactPath(Artifact.flutterTester);
+    if (!globals.processManager.canRun(shellPath)) {
+      throwToolExit('Cannot execute Flutter tester at $shellPath');
     }
+
+    // Compute the command-line arguments for package:test.
+    final List<String> testArgs = <String>[
+      if (!globals.terminal.supportsColor)
+        '--no-color',
+      if (startPaused)
+        '--pause-after-load',
+      if (machine)
+        ...<String>['-r', 'json']
+      else
+        ...<String>['-r', 'compact'],
+      '--concurrency=$concurrency',
+      for (final String name in names)
+        ...<String>['--name', name],
+      for (final String plainName in plainNames)
+        ...<String>['--plain-name', plainName],
+      '--test-randomize-ordering-seed=$randomSeed',
+    ];
+    if (web) {
+      final String tempBuildDir = globals.fs.systemTempDirectory
+        .createTempSync('flutter_test.')
+        .absolute
+        .uri
+        .toFilePath();
+      final bool result = await webCompilationProxy.initialize(
+        projectDirectory: flutterProject.directory,
+        testOutputDir: tempBuildDir,
+        testFiles: testFiles,
+        projectName: flutterProject.manifest.appName,
+        initializePlatform: true,
+      );
+      if (!result) {
+        throwToolExit('Failed to compile tests');
+      }
+      testArgs
+        ..add('--platform=chrome')
+        ..add('--precompiled=$tempBuildDir')
+        ..add('--')
+        ..addAll(testFiles);
+      testWrapper.registerPlatformPlugin(
+        <Runtime>[Runtime.chrome],
+        () {
+          return FlutterWebPlatform.start(
+            flutterProject.directory.path,
+            updateGoldens: updateGoldens,
+            shellPath: shellPath,
+            flutterProject: flutterProject,
+            pauseAfterLoad: startPaused,
+          );
+        },
+      );
+      await testWrapper.main(testArgs);
+      return exitCode;
+    }
+
     testArgs
-      ..add('--platform=chrome')
-      ..add('--precompiled=$tempBuildDir')
       ..add('--')
       ..addAll(testFiles);
-    testWrapper.registerPlatformPlugin(
-      <Runtime>[Runtime.chrome],
-      () {
-        return FlutterWebPlatform.start(
-          flutterProject.directory.path,
-          updateGoldens: updateGoldens,
-          shellPath: shellPath,
-          flutterProject: flutterProject,
-          pauseAfterLoad: startPaused,
-        );
-      },
+
+    final InternetAddressType serverType =
+        ipv6 ? InternetAddressType.IPv6 : InternetAddressType.IPv4;
+
+    final loader.FlutterPlatform platform = loader.installHook(
+      testWrapper: testWrapper,
+      shellPath: shellPath,
+      watcher: watcher,
+      enableObservatory: enableObservatory,
+      machine: machine,
+      startPaused: startPaused,
+      disableServiceAuthCodes: disableServiceAuthCodes,
+      serverType: serverType,
+      precompiledDillPath: precompiledDillPath,
+      precompiledDillFiles: precompiledDillFiles,
+      buildMode: buildMode,
+      trackWidgetCreation: trackWidgetCreation,
+      updateGoldens: updateGoldens,
+      buildTestAssets: buildTestAssets,
+      projectRootDirectory: globals.fs.currentDirectory.uri,
+      flutterProject: flutterProject,
+      icudtlPath: icudtlPath,
     );
-    await testWrapper.main(testArgs);
-    return exitCode;
-  }
 
-  testArgs
-    ..add('--')
-    ..addAll(testFiles);
+    // Make the global packages path absolute.
+    // (Makes sure it still works after we change the current directory.)
+    PackageMap.globalPackagesPath =
+        globals.fs.path.normalize(globals.fs.path.absolute(PackageMap.globalPackagesPath));
 
-  final InternetAddressType serverType =
-      ipv6 ? InternetAddressType.IPv6 : InternetAddressType.IPv4;
+    // Call package:test's main method in the appropriate directory.
+    final Directory saved = globals.fs.currentDirectory;
+    try {
+      if (workDir != null) {
+        globals.printTrace('switching to directory $workDir to run tests');
+        globals.fs.currentDirectory = workDir;
+      }
 
-  final loader.FlutterPlatform platform = loader.installHook(
-    testWrapper: testWrapper,
-    shellPath: shellPath,
-    watcher: watcher,
-    enableObservatory: enableObservatory,
-    machine: machine,
-    startPaused: startPaused,
-    disableServiceAuthCodes: disableServiceAuthCodes,
-    serverType: serverType,
-    precompiledDillPath: precompiledDillPath,
-    precompiledDillFiles: precompiledDillFiles,
-    buildMode: buildMode,
-    trackWidgetCreation: trackWidgetCreation,
-    updateGoldens: updateGoldens,
-    buildTestAssets: buildTestAssets,
-    projectRootDirectory: globals.fs.currentDirectory.uri,
-    flutterProject: flutterProject,
-    icudtlPath: icudtlPath,
-  );
+      globals.printTrace('running test package with arguments: $testArgs');
+      await testWrapper.main(testArgs);
 
-  // Make the global packages path absolute.
-  // (Makes sure it still works after we change the current directory.)
-  PackageMap.globalPackagesPath =
-      globals.fs.path.normalize(globals.fs.path.absolute(PackageMap.globalPackagesPath));
+      // test.main() sets dart:io's exitCode global.
+      globals.printTrace('test package returned with exit code $exitCode');
 
-  // Call package:test's main method in the appropriate directory.
-  final Directory saved = globals.fs.currentDirectory;
-  try {
-    if (workDir != null) {
-      globals.printTrace('switching to directory $workDir to run tests');
-      globals.fs.currentDirectory = workDir;
+      return exitCode;
+    } finally {
+      globals.fs.currentDirectory = saved;
+      await platform.close();
     }
-
-    globals.printTrace('running test package with arguments: $testArgs');
-    await testWrapper.main(testArgs);
-
-    // test.main() sets dart:io's exitCode global.
-    globals.printTrace('test package returned with exit code $exitCode');
-
-    return exitCode;
-  } finally {
-    globals.fs.currentDirectory = saved;
-    await platform.close();
   }
 }
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart
index b8145cf..792a0c8 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart
@@ -7,9 +7,13 @@
 import 'package:args/command_runner.dart';
 import 'package:file/memory.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/commands/test.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:flutter_tools/src/test/runner.dart';
 import 'package:flutter_tools/src/test/test_wrapper.dart';
+import 'package:flutter_tools/src/test/watcher.dart';
 import 'package:process/process.dart';
 
 import '../../src/common.dart';
@@ -73,6 +77,92 @@
     ProcessManager: () => FakeProcessManager.any(),
     Cache: () => FakeCache(),
   });
+
+  testUsingContext('Pipes enable-observatory', () async {
+    final FakeFlutterTestRunner testRunner = FakeFlutterTestRunner(0);
+
+    final TestCommand testCommand = TestCommand(testRunner: testRunner);
+    final CommandRunner<void> commandRunner =
+        createTestCommandRunner(testCommand);
+
+    await commandRunner.run(const <String>[
+      'test',
+      '--no-pub',
+      '--enable-vmservice',
+      '--',
+      'test/fake_test.dart',
+    ]);
+    expect(
+      testRunner.lastEnableObservatoryValue,
+      true,
+    );
+
+    await commandRunner.run(const <String>[
+      'test',
+      '--no-pub',
+      '--start-paused',
+      '--no-enable-vmservice',
+      '--',
+      'test/fake_test.dart',
+    ]);
+    expect(
+      testRunner.lastEnableObservatoryValue,
+      true,
+    );
+
+    await commandRunner.run(const <String>[
+      'test',
+      '--no-pub',
+      '--',
+      'test/fake_test.dart',
+    ]);
+    expect(
+      testRunner.lastEnableObservatoryValue,
+      false,
+    );
+  }, overrides: <Type, Generator>{
+    FileSystem: () => fs,
+    ProcessManager: () => FakeProcessManager.any(),
+    Cache: () => FakeCache(),
+  });
+}
+
+class FakeFlutterTestRunner implements FlutterTestRunner {
+  FakeFlutterTestRunner(this.exitCode);
+
+  int exitCode;
+  bool lastEnableObservatoryValue;
+
+  @override
+  Future<int> runTests(
+    TestWrapper testWrapper,
+    List<String> testFiles, {
+    Directory workDir,
+    List<String> names = const <String>[],
+    List<String> plainNames = const <String>[],
+    bool enableObservatory = false,
+    bool startPaused = false,
+    bool disableServiceAuthCodes = false,
+    bool ipv6 = false,
+    bool machine = false,
+    String precompiledDillPath,
+    Map<String, String> precompiledDillFiles,
+    BuildMode buildMode,
+    bool trackWidgetCreation = false,
+    bool updateGoldens = false,
+    TestWatcher watcher,
+    int concurrency,
+    bool buildTestAssets = false,
+    FlutterProject flutterProject,
+    String icudtlPath,
+    Directory coverageDirectory,
+    bool web = false,
+    String randomSeed = '0',
+  }) async {
+    lastEnableObservatoryValue = enableObservatory;
+    return exitCode;
+  }
+
 }
 
 class FakePackageTest implements TestWrapper {