blob: 64980020eef0322c93837bc0e25fa4f855e451aa [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. 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 'dart:convert';
import 'dart:io';
import 'package:async/async.dart';
import 'package:path/path.dart' as p;
import 'package:pool/pool.dart';
import 'package:test_api/backend.dart'; // ignore: deprecated_member_use
import 'package:frontend_server_client/frontend_server_client.dart';
import '../package_version.dart';
import '../../util/dart.dart';
import '../../util/package_config.dart';
class CompilationResponse {
final String? compilerOutput;
final int errorCount;
final Uri? kernelOutputUri;
const CompilationResponse(
{this.compilerOutput, this.errorCount = 0, this.kernelOutputUri});
static const _wasShutdown = CompilationResponse(
errorCount: 1, compilerOutput: 'Compiler no longer active.');
class TestCompiler {
final _closeMemo = AsyncMemoizer<void>();
/// Each language version that appears in test files gets its own compiler,
/// to ensure that all language modes are supported (such as sound and
/// unsound null safety).
final _compilerForLanguageVersion =
<String, _TestCompilerForLanguageVersion>{};
/// A prefix used for the dill files for each compiler that is created.
final String _dillCachePrefix;
/// No work is done until the first call to [compile] is recieved, at which
/// point the compiler process is started.
/// Compiles [mainDart], using a separate compiler per language version of
/// the tests.
Future<CompilationResponse> compile(Uri mainDart, Metadata metadata) async {
if (_closeMemo.hasRun) return CompilationResponse._wasShutdown;
var languageVersionComment = metadata.languageVersionComment ??
await rootPackageLanguageVersionComment;
var compiler = _compilerForLanguageVersion.putIfAbsent(
() => _TestCompilerForLanguageVersion(
_dillCachePrefix, languageVersionComment));
return compiler.compile(mainDart);
Future<void> dispose() => _closeMemo.runOnce(() => Future.wait([
for (var compiler in _compilerForLanguageVersion.values)
class _TestCompilerForLanguageVersion {
final _closeMemo = AsyncMemoizer();
final _compilePool = Pool(1);
final String _dillCachePath;
FrontendServerClient? _frontendServerClient;
final String _languageVersionComment;
late final _outputDill =
File(p.join(_outputDillDirectory.path, 'output.dill'));
final _outputDillDirectory =
String dillCachePrefix, this._languageVersionComment)
: _dillCachePath = '$dillCachePrefix.'
'${_dillCacheSuffix(_languageVersionComment, enabledExperiments)}';
String _generateEntrypoint(Uri testUri) {
return '''
import "dart:isolate";
import "package:test_core/src/bootstrap/vm.dart";
import "$testUri" as test;
void main(_, SendPort sendPort) {
internalBootstrapVmTest(() => test.main, sendPort);
Future<CompilationResponse> compile(Uri mainUri) =>
_compilePool.withResource(() => _compile(mainUri));
Future<CompilationResponse> _compile(Uri mainUri) async {
if (_closeMemo.hasRun) return CompilationResponse._wasShutdown;
var firstCompile = false;
CompileResult? compilerOutput;
final tempFile = File(p.join(_outputDillDirectory.path, 'test.dart'))
try {
if (_frontendServerClient == null) {
compilerOutput = await _createCompiler(tempFile.uri);
firstCompile = true;
} else {
compilerOutput =
await _frontendServerClient!.compile(<Uri>[tempFile.uri]);
} catch (e, s) {
if (_closeMemo.hasRun) return CompilationResponse._wasShutdown;
return CompilationResponse(errorCount: 1, compilerOutput: '$e\n$s');
} finally {
// The client is guaranteed initialized at this point.
final outputPath = compilerOutput?.dillOutput;
if (outputPath == null) {
return CompilationResponse(
compilerOutput: compilerOutput?.compilerOutputLines.join('\n'),
errorCount: compilerOutput?.errorCount ?? 0);
final outputFile = File(outputPath);
final kernelReadyToRun = await outputFile.copy('${tempFile.path}.dill');
final testCache = File(_dillCachePath);
// Keep the cache file up-to-date and use the size of the kernel file
// as an approximation for how many packages are included. Larger files
// are prefered, since re-using more packages will reduce the number of
// files the frontend server needs to load and parse.
if (firstCompile ||
!testCache.existsSync() ||
(testCache.lengthSync() < outputFile.lengthSync())) {
if (!testCache.parent.existsSync()) {
testCache.parent.createSync(recursive: true);
await outputFile.copy(_dillCachePath);
return CompilationResponse(
compilerOutput: compilerOutput?.compilerOutputLines.join('\n'),
errorCount: compilerOutput?.errorCount ?? 0,
kernelOutputUri: kernelReadyToRun.absolute.uri);
Future<CompileResult?> _createCompiler(Uri testUri) async {
final platformDill = 'lib/_internal/vm_platform_strong.dill';
final sdkRoot =
var client = _frontendServerClient = await FrontendServerClient.start(
enabledExperiments: enabledExperiments,
sdkRoot: sdkRoot,
packagesJson: (await packageConfigUri).toFilePath(),
printIncrementalDependencies: false,
return client.compile();
Future<void> dispose() => _closeMemo.runOnce(() async {
await _compilePool.close();
_frontendServerClient = null;
if (_outputDillDirectory.existsSync()) {
_outputDillDirectory.deleteSync(recursive: true);
/// Computes a unique dill cache suffix for each [languageVersionComment]
/// and [enabledExperiments] combination.
String _dillCacheSuffix(
String languageVersionComment, List<String> enabledExperiments) {
var identifierString =
StringBuffer(languageVersionComment.replaceAll(' ', ''));
for (var experiment in enabledExperiments) {
return base64.encode(utf8.encode(identifierString.toString()));