blob: 1433a7150c26037fad669241d59232f9bc9a0e71 [file] [log] [blame]
// 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 '../base/platform.dart';
import '../build_info.dart';
import '../bundle.dart';
import '../compile.dart';
import '../flutter_plugins.dart';
import '../globals.dart' as globals;
import '../linux/native_assets.dart';
import '../macos/native_assets.dart';
import '../native_assets.dart';
import '../project.dart';
import '../windows/native_assets.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,
frontendServerStarterPath: buildInfo.frontendServerStarterPath,
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);
}
Uri? nativeAssetsYaml;
if (!buildInfo.buildNativeAssets) {
nativeAssetsYaml = null;
} else {
final Uri projectUri = FlutterProject.current().directory.uri;
final NativeAssetsBuildRunner buildRunner = NativeAssetsBuildRunnerImpl(
projectUri,
buildInfo.packageConfig,
globals.fs,
globals.logger,
);
if (globals.platform.isMacOS) {
(nativeAssetsYaml, _) = await buildNativeAssetsMacOS(
buildMode: buildInfo.mode,
projectUri: projectUri,
flutterTester: true,
fileSystem: globals.fs,
buildRunner: buildRunner,
);
} else if (globals.platform.isLinux) {
(nativeAssetsYaml, _) = await buildNativeAssetsLinux(
buildMode: buildInfo.mode,
projectUri: projectUri,
flutterTester: true,
fileSystem: globals.fs,
buildRunner: buildRunner,
);
} else if (globals.platform.isWindows) {
(nativeAssetsYaml, _) = await buildNativeAssetsWindows(
buildMode: buildInfo.mode,
projectUri: projectUri,
flutterTester: true,
fileSystem: globals.fs,
buildRunner: buildRunner,
);
} else {
await ensureNoNativeAssetsOrOsIsSupported(
projectUri,
const LocalPlatform().operatingSystem,
globals.fs,
buildRunner,
);
}
}
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,
nativeAssetsYaml: nativeAssetsYaml,
);
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);
}
}
}