1// Copyright 2019 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6 7import 'package:meta/meta.dart'; 8 9import '../artifacts.dart'; 10import '../base/file_system.dart'; 11import '../base/terminal.dart'; 12import '../build_info.dart'; 13import '../bundle.dart'; 14import '../codegen.dart'; 15import '../compile.dart'; 16import '../dart/package_map.dart'; 17import '../globals.dart'; 18import '../project.dart'; 19 20/// A request to the [TestCompiler] for recompilation. 21class _CompilationRequest { 22 _CompilationRequest(this.path, this.result); 23 String path; 24 Completer<String> result; 25} 26 27/// A frontend_server wrapper for the flutter test runner. 28/// 29/// This class is a wrapper around compiler that allows multiple isolates to 30/// enqueue compilation requests, but ensures only one compilation at a time. 31class TestCompiler { 32 /// Creates a new [TestCompiler] which acts as a frontend_server proxy. 33 /// 34 /// [trackWidgetCreation] configures whether the kernel transform is applied 35 /// to the output. This also changes the output file to include a '.track` 36 /// extension. 37 /// 38 /// [flutterProject] is the project for which we are running tests. 39 TestCompiler( 40 this.trackWidgetCreation, 41 this.flutterProject, 42 ) : testFilePath = getKernelPathForTransformerOptions( 43 fs.path.join(flutterProject.directory.path, getBuildDirectory(), 'testfile.dill'), 44 trackWidgetCreation: trackWidgetCreation, 45 ) { 46 // Compiler maintains and updates single incremental dill file. 47 // Incremental compilation requests done for each test copy that file away 48 // for independent execution. 49 final Directory outputDillDirectory = fs.systemTempDirectory.createTempSync('flutter_test_compiler.'); 50 outputDill = outputDillDirectory.childFile('output.dill'); 51 printTrace('Compiler will use the following file as its incremental dill file: ${outputDill.path}'); 52 printTrace('Listening to compiler controller...'); 53 compilerController.stream.listen(_onCompilationRequest, onDone: () { 54 printTrace('Deleting ${outputDillDirectory.path}...'); 55 outputDillDirectory.deleteSync(recursive: true); 56 }); 57 } 58 59 final StreamController<_CompilationRequest> compilerController = StreamController<_CompilationRequest>(); 60 final List<_CompilationRequest> compilationQueue = <_CompilationRequest>[]; 61 final FlutterProject flutterProject; 62 final bool trackWidgetCreation; 63 final String testFilePath; 64 65 66 ResidentCompiler compiler; 67 File outputDill; 68 // Whether to report compiler messages. 69 bool _suppressOutput = false; 70 71 Future<String> compile(String mainDart) { 72 final Completer<String> completer = Completer<String>(); 73 compilerController.add(_CompilationRequest(mainDart, completer)); 74 return completer.future; 75 } 76 77 Future<void> _shutdown() async { 78 // Check for null in case this instance is shut down before the 79 // lazily-created compiler has been created. 80 if (compiler != null) { 81 await compiler.shutdown(); 82 compiler = null; 83 } 84 } 85 86 Future<void> dispose() async { 87 await compilerController.close(); 88 await _shutdown(); 89 } 90 91 /// Create the resident compiler used to compile the test. 92 @visibleForTesting 93 Future<ResidentCompiler> createCompiler() async { 94 if (flutterProject.hasBuilders) { 95 return CodeGeneratingResidentCompiler.create( 96 flutterProject: flutterProject, 97 trackWidgetCreation: trackWidgetCreation, 98 compilerMessageConsumer: _reportCompilerMessage, 99 initializeFromDill: testFilePath, 100 // We already ran codegen once at the start, we only need to 101 // configure builders. 102 runCold: true, 103 ); 104 } 105 return ResidentCompiler( 106 artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath), 107 packagesPath: PackageMap.globalPackagesPath, 108 trackWidgetCreation: trackWidgetCreation, 109 compilerMessageConsumer: _reportCompilerMessage, 110 initializeFromDill: testFilePath, 111 unsafePackageSerialization: false, 112 ); 113 } 114 115 // Handle a compilation request. 116 Future<void> _onCompilationRequest(_CompilationRequest request) async { 117 final bool isEmpty = compilationQueue.isEmpty; 118 compilationQueue.add(request); 119 // Only trigger processing if queue was empty - i.e. no other requests 120 // are currently being processed. This effectively enforces "one 121 // compilation request at a time". 122 if (!isEmpty) { 123 return; 124 } 125 while (compilationQueue.isNotEmpty) { 126 final _CompilationRequest request = compilationQueue.first; 127 printTrace('Compiling ${request.path}'); 128 final Stopwatch compilerTime = Stopwatch()..start(); 129 bool firstCompile = false; 130 if (compiler == null) { 131 compiler = await createCompiler(); 132 firstCompile = true; 133 } 134 _suppressOutput = false; 135 final CompilerOutput compilerOutput = await compiler.recompile( 136 request.path, 137 <Uri>[Uri.parse(request.path)], 138 outputPath: outputDill.path, 139 ); 140 final String outputPath = compilerOutput?.outputFilename; 141 142 // In case compiler didn't produce output or reported compilation 143 // errors, pass [null] upwards to the consumer and shutdown the 144 // compiler to avoid reusing compiler that might have gotten into 145 // a weird state. 146 if (outputPath == null || compilerOutput.errorCount > 0) { 147 request.result.complete(null); 148 await _shutdown(); 149 } else { 150 final File outputFile = fs.file(outputPath); 151 final File kernelReadyToRun = await outputFile.copy('${request.path}.dill'); 152 final File testCache = fs.file(testFilePath); 153 if (firstCompile || !testCache.existsSync() || (testCache.lengthSync() < outputFile.lengthSync())) { 154 // The idea is to keep the cache file up-to-date and include as 155 // much as possible in an effort to re-use as many packages as 156 // possible. 157 ensureDirectoryExists(testFilePath); 158 await outputFile.copy(testFilePath); 159 } 160 request.result.complete(kernelReadyToRun.path); 161 compiler.accept(); 162 compiler.reset(); 163 } 164 printTrace('Compiling ${request.path} took ${compilerTime.elapsedMilliseconds}ms'); 165 // Only remove now when we finished processing the element 166 compilationQueue.removeAt(0); 167 } 168 } 169 170 void _reportCompilerMessage(String message, {bool emphasis, TerminalColor color}) { 171 if (_suppressOutput) { 172 return; 173 } 174 if (message.startsWith('Error: Could not resolve the package \'flutter_test\'')) { 175 printTrace(message); 176 printError('\n\nFailed to load test harness. Are you missing a dependency on flutter_test?\n', 177 emphasis: emphasis, 178 color: color, 179 ); 180 _suppressOutput = true; 181 return; 182 } 183 printError('$message'); 184 } 185} 186