| // 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. | |
| 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 '../flutter_plugins.dart'; | |
| import '../globals.dart' as globals; | |
| import '../project.dart'; | |
| import 'test_time_recorder.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. | |
| /// | |
| /// If [precompiledDillPath] is passed, it will be used to initialize the | |
| /// compiler. | |
| /// | |
| /// If [testTimeRecorder] is passed, times will be recorded in it. | |
| TestCompiler( | |
| this.buildInfo, | |
| this.flutterProject, | |
| { String? precompiledDillPath, this.testTimeRecorder } | |
| ) : testFilePath = precompiledDillPath ?? globals.fs.path.join( | |
| flutterProject!.directory.path, | |
| getBuildDirectory(), | |
| 'test_cache', | |
| getDefaultCachedKernelPath( | |
| trackWidgetCreation: buildInfo.trackWidgetCreation, | |
| dartDefines: buildInfo.dartDefines, | |
| extraFrontEndOptions: buildInfo.extraFrontEndOptions, | |
| )), | |
| shouldCopyDillFile = precompiledDillPath == 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 = 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; | |
| final bool shouldCopyDillFile; | |
| final TestTimeRecorder? testTimeRecorder; | |
| ResidentCompiler? compiler; | |
| late File outputDill; | |
| Future<String?> compile(Uri mainDart) { | |
| final Completer<String?> completer = Completer<String?>(); | |
| if (compilerController.isClosed) { | |
| return Future<String?>.value(); | |
| } | |
| 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, | |
| dartDefines: buildInfo.dartDefines, | |
| packagesPath: buildInfo.packagesPath, | |
| extraFrontEndOptions: buildInfo.extraFrontEndOptions, | |
| platform: globals.platform, | |
| testCompilation: true, | |
| fileSystem: globals.fs, | |
| fileSystemRoots: buildInfo.fileSystemRoots, | |
| fileSystemScheme: buildInfo.fileSystemScheme, | |
| ); | |
| 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(); | |
| final Stopwatch? testTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.Compile); | |
| bool firstCompile = false; | |
| if (compiler == null) { | |
| compiler = await createCompiler(); | |
| firstCompile = true; | |
| } | |
| final List<Uri> invalidatedRegistrantFiles = <Uri>[]; | |
| if (flutterProject != null) { | |
| // Update the generated registrant to use the test target's main. | |
| final String mainUriString = buildInfo.packageConfig.toPackageUri(request.mainUri)?.toString() | |
| ?? request.mainUri.toString(); | |
| await generateMainDartWithPluginRegistrant( | |
| flutterProject!, | |
| buildInfo.packageConfig, | |
| mainUriString, | |
| globals.fs.file(request.mainUri), | |
| ); | |
| invalidatedRegistrantFiles.add(flutterProject!.dartPluginRegistrant.absolute.uri); | |
| } | |
| final CompilerOutput? compilerOutput = await compiler!.recompile( | |
| request.mainUri, | |
| <Uri>[request.mainUri, ...invalidatedRegistrantFiles], | |
| outputPath: outputDill.path, | |
| packageConfig: buildInfo.packageConfig, | |
| projectRootPath: flutterProject?.directory.absolute.path, | |
| checkDartPluginRegistry: true, | |
| 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. | |
| if (outputPath == null || compilerOutput!.errorCount > 0) { | |
| request.result.complete(); | |
| await _shutdown(); | |
| } else { | |
| if (shouldCopyDillFile) { | |
| final String path = request.mainUri.toFilePath(windows: globals.platform.isWindows); | |
| 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); | |
| } else { | |
| request.result.complete(outputPath); | |
| } | |
| compiler!.accept(); | |
| compiler!.reset(); | |
| } | |
| globals.printTrace('Compiling ${request.mainUri} took ${compilerTime.elapsedMilliseconds}ms'); | |
| testTimeRecorder?.stop(TestTimePhases.Compile, testTimeRecorderStopwatch!); | |
| // Only remove now when we finished processing the element | |
| compilationQueue.removeAt(0); | |
| } | |
| } | |
| } |