Create frontend compiler in `flutter test` lazily. (#15251)

* Create frontend compiler in `flutter test` lazily.

This is needed to avoid unwarranted runtime dependencies on Artifacts,
for example when in non-preview-dart-2 mode.
Refactor the compilation-related code into _Compiler class.

diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart
index 7faf749..af43f3e 100644
--- a/packages/flutter_tools/lib/src/test/flutter_platform.dart
+++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart
@@ -60,7 +60,7 @@
   bool startPaused: false,
   bool previewDart2: false,
   int port: 0,
-  String dillFilePath,
+  String precompiledDillPath,
   int observatoryPort,
   InternetAddressType serverType: InternetAddressType.IP_V4,
 }) {
@@ -78,7 +78,7 @@
       host: _kHosts[serverType],
       previewDart2: previewDart2,
       port: port,
-      dillFilePath: dillFilePath,
+      precompiledDillPath: precompiledDillPath,
     ),
   );
 }
@@ -87,11 +87,66 @@
 enum _TestResult { crashed, harnessBailed, testBailed }
 typedef Future<Null> _Finalizer();
 
-class CompilationRequest {
+class _CompilationRequest {
   String path;
   Completer<String> result;
 
-  CompilationRequest(this.path, this.result);
+  _CompilationRequest(this.path, this.result);
+}
+
+// This class is a wrapper around compiler that allows multiple isolates to
+// enqueue compilation requests, but ensures only one compilation at a time.
+class _Compiler {
+  _Compiler() {
+    // Compiler maintains and updates single incremental dill file.
+    // Incremental compilation requests done for each test copy that file away
+    // for independent execution.
+    final Directory outputDillDirectory = fs.systemTempDirectory
+        .createTempSync('output_dill');
+    final File outputDill = outputDillDirectory.childFile('output.dill');
+
+    compilerController.stream.listen((_CompilationRequest request) async {
+      final bool isEmpty = compilationQueue.isEmpty;
+      compilationQueue.add(request);
+      // Only trigger processing if queue was empty - i.e. no other requests
+      // are currently being processed. This effectively enforces "one
+      // compilation request at a time".
+      if (isEmpty) {
+        while (compilationQueue.isNotEmpty) {
+          final _CompilationRequest request = compilationQueue.first;
+          printTrace('Compiling ${request.path}');
+          final String outputPath = await compiler.recompile(request.path,
+            <String>[request.path],
+            outputPath: outputDill.path,
+          );
+          // Copy output dill next to the source file.
+          await outputDill.copy(request.path + '.dill');
+          compiler.accept();
+          compiler.reset();
+          request.result.complete(outputPath);
+          // Only remove now when we finished processing the element
+          compilationQueue.removeAt(0);
+        }
+      }
+    }, onDone: () {
+      outputDillDirectory.deleteSync(recursive: true);
+    });
+
+    compiler = new ResidentCompiler(
+        artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath),
+        packagesPath: PackageMap.globalPackagesPath);
+  }
+
+  final StreamController<_CompilationRequest> compilerController =
+      new StreamController<_CompilationRequest>();
+  final List<_CompilationRequest> compilationQueue = <_CompilationRequest>[];
+  ResidentCompiler compiler;
+
+  Future<String> compile(String mainDart) {
+    final Completer<String> completer = new Completer<String>();
+    compilerController.add(new _CompilationRequest(mainDart, completer));
+    return completer.future;
+  }
 }
 
 class _FlutterPlatform extends PlatformPlugin {
@@ -105,43 +160,8 @@
     this.host,
     this.previewDart2,
     this.port,
-    this.dillFilePath,
-  }) : assert(shellPath != null) {
-
-    // Compiler maintains and updates single incremental dill file.
-    // Incremental compilation requests done for each test copy that file away
-    // for independent execution.
-    final Directory outputDillDirectory = fs.systemTempDirectory
-        .createTempSync('output_dill');
-    final File outputDill = outputDillDirectory.childFile('output.dill');
-
-    compilerController.stream.listen((CompilationRequest request) async {
-      final bool isEmpty = compilationQueue.isEmpty;
-      compilationQueue.add(request);
-      // Only trigger processing if queue was empty - i.e. no other requests
-      // are currently being processed. This effectively enforces "one
-      // compilation request at a time".
-      if (isEmpty) {
-        while (compilationQueue.isNotEmpty) {
-          final CompilationRequest request = compilationQueue.first;
-          printTrace('Compiling ${request.path}');
-          final String outputPath = await compiler.recompile(request.path,
-            <String>[request.path],
-            outputPath: outputDill.path
-          );
-          // Copy output dill next to the source file.
-          await outputDill.copy(request.path + '.dill');
-          compiler.accept();
-          compiler.reset();
-          request.result.complete(outputPath);
-          // Only remove now when we finished processing the element
-          compilationQueue.removeAt(0);
-        }
-      }
-    }, onDone: () {
-      outputDillDirectory.delete(recursive: true);
-    });
-  }
+    this.precompiledDillPath,
+  }) : assert(shellPath != null);
 
   final String shellPath;
   final TestWatcher watcher;
@@ -152,13 +172,9 @@
   final InternetAddress host;
   final bool previewDart2;
   final int port;
-  final String dillFilePath;
-  final StreamController<CompilationRequest> compilerController =
-      new StreamController<CompilationRequest>();
-  ResidentCompiler compiler =
-      new ResidentCompiler(artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath),
-          packagesPath: PackageMap.globalPackagesPath);
-  final List<CompilationRequest> compilationQueue = <CompilationRequest>[];
+  final String precompiledDillPath;
+
+  _Compiler compiler;
 
   // Each time loadChannel() is called, we spin up a local WebSocket server,
   // then spin up the engine in a subprocess. We pass the engine a Dart file
@@ -176,7 +192,7 @@
       if (explicitObservatoryPort != null)
         throwToolExit('installHook() was called with an observatory port or debugger mode enabled, but then more than one test suite was run.');
       // Fail if we're passing in a precompiled entry-point.
-      if (dillFilePath != null)
+      if (precompiledDillPath != null)
         throwToolExit('installHook() was called with a precompiled test entry-point, but then more than one test suite was run.');
     }
     final int ourTestCount = _testCount;
@@ -240,71 +256,25 @@
         cancelOnError: true,
       );
 
-      // Prepare a temporary directory to store the Dart file that will talk to us.
-      // If a kernel file is given, then use that to launch the test.
-      File listenerFile;
-      if (dillFilePath == null) {
-        final Directory temporaryDirectory = fs.systemTempDirectory
-            .createTempSync('dart_test_listener');
-        finalizers.add(() async {
-          printTrace('test $ourTestCount: deleting temporary directory');
-          temporaryDirectory.deleteSync(recursive: true);
-        });
-
-        // Prepare the Dart file that will talk to us and start the test.
-        listenerFile = fs.file(
-            '${temporaryDirectory.path}/listener.dart');
-        listenerFile.createSync();
-        listenerFile.writeAsStringSync(_generateTestMain(
-          testUrl: fs.path.toUri(fs.path.absolute(testPath)).toString(),
-          encodedWebsocketUrl: Uri.encodeComponent(_getWebSocketUrl(server)),
-        ));
-      }
-      // Start the engine subprocess.
       printTrace('test $ourTestCount: starting shell process${previewDart2? " in preview-dart-2 mode":""}');
 
-      String mainDart = listenerFile?.path ?? testPath;
-      String bundlePath;
+      // [precompiledDillPath] can be set only if [previewDart2] is [true].
+      assert(precompiledDillPath == null || previewDart2);
+      // If a kernel file is given, then use that to launch the test.
+      // Otherwise create a "listener" dart that invokes actual test.
+      String mainDart = precompiledDillPath != null
+          ? precompiledDillPath
+          : _createListenerDart(finalizers, ourTestCount, testPath, server);
 
-      if (previewDart2) {
-        if (dillFilePath == null) {
-          final Completer<String> completer = new Completer<String>();
-          compilerController.add(
-              new CompilationRequest(listenerFile.path, completer));
-          mainDart = await completer.future;
+      if (previewDart2 && precompiledDillPath == null) {
+        // Lazily instantiate compiler so it is built only if it is actually used.
+        compiler ??= new _Compiler();
+        mainDart = await compiler.compile(mainDart);
 
-          if (mainDart == null) {
-            controller.sink.addError(
-                _getErrorMessage('Compilation failed', testPath, shellPath));
-            return null;
-          }
-
-          // bundlePath needs to point to a folder with `platform.dill` file.
-          final Directory tempBundleDirectory = fs.systemTempDirectory
-              .createTempSync('flutter_bundle_directory');
-          finalizers.add(() async {
-            printTrace(
-                'test $ourTestCount: deleting temporary bundle directory');
-            tempBundleDirectory.deleteSync(recursive: true);
-          });
-
-          // copy 'vm_platform_strong.dill' into 'platform.dill'
-          final File vmPlatformStrongDill = fs.file(
-            artifacts.getArtifactPath(Artifact.platformKernelDill),
-          );
-          final File platformDill = vmPlatformStrongDill.copySync(
-            tempBundleDirectory
-                .childFile('platform.dill')
-                .path,
-          );
-          if (!platformDill.existsSync()) {
-            printError('unexpected error copying platform kernel file');
-          }
-
-          bundlePath = tempBundleDirectory.path;
-        } else {
-          mainDart = dillFilePath;
-          bundlePath = artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath);
+        if (mainDart == null) {
+          controller.sink.addError(
+              _getErrorMessage('Compilation failed', testPath, shellPath));
+          return null;
         }
       }
 
@@ -314,7 +284,7 @@
         packages: PackageMap.globalPackagesPath,
         enableObservatory: enableObservatory,
         startPaused: startPaused,
-        bundlePath: bundlePath,
+        bundlePath: _getBundlePath(finalizers, ourTestCount),
         observatoryPort: explicitObservatoryPort,
       );
       subprocessActive = true;
@@ -519,6 +489,60 @@
     return null;
   }
 
+  String _createListenerDart(List<_Finalizer> finalizers, int ourTestCount,
+      String testPath, HttpServer server) {
+    // Prepare a temporary directory to store the Dart file that will talk to us.
+    final Directory temporaryDirectory = fs.systemTempDirectory
+        .createTempSync('dart_test_listener');
+    finalizers.add(() async {
+      printTrace('test $ourTestCount: deleting temporary directory');
+      temporaryDirectory.deleteSync(recursive: true);
+    });
+
+    // Prepare the Dart file that will talk to us and start the test.
+    final File listenerFile = fs.file('${temporaryDirectory.path}/listener.dart');
+    listenerFile.createSync();
+    listenerFile.writeAsStringSync(_generateTestMain(
+      testUrl: fs.path.toUri(fs.path.absolute(testPath)).toString(),
+      encodedWebsocketUrl: Uri.encodeComponent(_getWebSocketUrl(server))
+    ));
+    return listenerFile.path;
+  }
+
+  String _getBundlePath(List<_Finalizer> finalizers, int ourTestCount) {
+    if (!previewDart2) {
+      return null;
+    }
+
+    if (precompiledDillPath != null) {
+      return artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath);
+    }
+
+    // bundlePath needs to point to a folder with `platform.dill` file.
+    final Directory tempBundleDirectory = fs.systemTempDirectory
+        .createTempSync('flutter_bundle_directory');
+    finalizers.add(() async {
+      printTrace(
+          'test $ourTestCount: deleting temporary bundle directory');
+      tempBundleDirectory.deleteSync(recursive: true);
+    });
+
+    // copy 'vm_platform_strong.dill' into 'platform.dill'
+    final File vmPlatformStrongDill = fs.file(
+      artifacts.getArtifactPath(Artifact.platformKernelDill),
+    );
+    final File platformDill = vmPlatformStrongDill.copySync(
+      tempBundleDirectory
+          .childFile('platform.dill')
+          .path,
+    );
+    if (!platformDill.existsSync()) {
+      printError('unexpected error copying platform kernel file');
+    }
+
+    return tempBundleDirectory.path;
+  }
+
   String _getWebSocketUrl(HttpServer server) {
     return host.type == InternetAddressType.IP_V4
         ? 'ws://${host.address}:${server.port}'