// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // @dart = 2.8 import 'dart:async'; import 'package:meta/meta.dart'; import '../artifacts.dart'; import '../base/file_system.dart'; import '../build_info.dart'; import '../bundle.dart'; import '../compile.dart'; import '../globals.dart' as globals; import '../project.dart'; /// A request to the [TestCompiler] for recompilation. class _CompilationRequest { _CompilationRequest(this.mainUri, this.result); Uri mainUri; Completer<String> result; } /// A frontend_server wrapper for the flutter test runner. /// /// This class is a wrapper around compiler that allows multiple isolates to /// enqueue compilation requests, but ensures only one compilation at a time. class TestCompiler { /// Creates a new [TestCompiler] which acts as a frontend_server proxy. /// /// [trackWidgetCreation] configures whether the kernel transform is applied /// to the output. This also changes the output file to include a '.track` /// extension. /// /// [flutterProject] is the project for which we are running tests. TestCompiler( this.buildInfo, this.flutterProject, ) : testFilePath = globals.fs.path.join( flutterProject.directory.path, getBuildDirectory(), 'test_cache', getDefaultCachedKernelPath( trackWidgetCreation: buildInfo.trackWidgetCreation, dartDefines: buildInfo.dartDefines, extraFrontEndOptions: buildInfo.extraFrontEndOptions, )) { // 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 = globals.fs.systemTempDirectory.createTempSync('flutter_test_compiler.'); outputDill = outputDillDirectory.childFile('output.dill'); globals.printTrace('Compiler will use the following file as its incremental dill file: ${outputDill.path}'); globals.printTrace('Listening to compiler controller...'); compilerController.stream.listen(_onCompilationRequest, onDone: () { globals.printTrace('Deleting ${outputDillDirectory.path}...'); outputDillDirectory.deleteSync(recursive: true); }); } final StreamController<_CompilationRequest> compilerController = StreamController<_CompilationRequest>(); final List<_CompilationRequest> compilationQueue = <_CompilationRequest>[]; final FlutterProject flutterProject; final BuildInfo buildInfo; final String testFilePath; ResidentCompiler compiler; File outputDill; Future<String> compile(Uri mainDart) { final Completer<String> completer = Completer<String>(); if (compilerController.isClosed) { return null; } compilerController.add(_CompilationRequest(mainDart, completer)); return completer.future; } Future<void> _shutdown() async { // Check for null in case this instance is shut down before the // lazily-created compiler has been created. if (compiler != null) { await compiler.shutdown(); compiler = null; } } Future<void> dispose() async { await compilerController.close(); await _shutdown(); } /// Create the resident compiler used to compile the test. @visibleForTesting Future<ResidentCompiler> createCompiler() async { final ResidentCompiler residentCompiler = ResidentCompiler( globals.artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath), artifacts: globals.artifacts, logger: globals.logger, processManager: globals.processManager, buildMode: buildInfo.mode, trackWidgetCreation: buildInfo.trackWidgetCreation, initializeFromDill: testFilePath, unsafePackageSerialization: false, dartDefines: buildInfo.dartDefines, packagesPath: buildInfo.packagesPath, extraFrontEndOptions: buildInfo.extraFrontEndOptions, platform: globals.platform, testCompilation: true, fileSystem: globals.fs, ); return residentCompiler; } // Handle a compilation request. Future<void> _onCompilationRequest(_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) { return; } while (compilationQueue.isNotEmpty) { final _CompilationRequest request = compilationQueue.first; globals.printTrace('Compiling ${request.mainUri}'); final Stopwatch compilerTime = Stopwatch()..start(); bool firstCompile = false; if (compiler == null) { compiler = await createCompiler(); firstCompile = true; } final CompilerOutput compilerOutput = await compiler.recompile( request.mainUri, <Uri>[request.mainUri], outputPath: outputDill.path, packageConfig: buildInfo.packageConfig, projectRootPath: flutterProject.directory.absolute.path, fs: globals.fs, ); final String outputPath = compilerOutput?.outputFilename; // In case compiler didn't produce output or reported compilation // errors, pass [null] upwards to the consumer and shutdown the // compiler to avoid reusing compiler that might have gotten into // a weird state. final String path = request.mainUri.toFilePath(windows: globals.platform.isWindows); if (outputPath == null || compilerOutput.errorCount > 0) { request.result.complete(null); await _shutdown(); } else { final File outputFile = globals.fs.file(outputPath); final File kernelReadyToRun = await outputFile.copy('$path.dill'); final File testCache = globals.fs.file(testFilePath); if (firstCompile || !testCache.existsSync() || (testCache.lengthSync() < outputFile.lengthSync())) { // The idea is to keep the cache file up-to-date and include as // much as possible in an effort to re-use as many packages as // possible. if (!testCache.parent.existsSync()) { testCache.parent.createSync(recursive: true); } await outputFile.copy(testFilePath); } request.result.complete(kernelReadyToRun.path); compiler.accept(); compiler.reset(); } globals.printTrace('Compiling $path took ${compilerTime.elapsedMilliseconds}ms'); // Only remove now when we finished processing the element compilationQueue.removeAt(0); } } }