blob: 8a9335158c7a1f05e9c7c239932c58baf612cc2f [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/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.
TestCompiler(this._dillCachePrefix);
/// 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(
languageVersionComment,
() => _TestCompilerForLanguageVersion(
_dillCachePrefix, languageVersionComment));
return compiler.compile(mainDart);
}
Future<void> dispose() => _closeMemo.runOnce(() => Future.wait([
for (var compiler in _compilerForLanguageVersion.values)
compiler.dispose(),
]));
}
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 =
Directory.systemTemp.createTempSync('dart_test.');
_TestCompilerForLanguageVersion(
String dillCachePrefix, this._languageVersionComment)
: _dillCachePath =
'$dillCachePrefix.${base64.encode(utf8.encode(_languageVersionComment.replaceAll(' ', '')))}';
String _generateEntrypoint(Uri testUri) {
return '''
$_languageVersionComment
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'))
..writeAsStringSync(_generateEntrypoint(mainUri));
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 {
_frontendServerClient?.accept();
_frontendServerClient?.reset();
}
// 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 =
p.relative(p.dirname(p.dirname(Platform.resolvedExecutable)));
var client = _frontendServerClient = await FrontendServerClient.start(
testUri.toString(),
_outputDill.path,
platformDill,
sdkRoot: sdkRoot,
packagesJson: (await packageConfigUri).toFilePath(),
printIncrementalDependencies: false,
);
return client.compile();
}
Future<void> dispose() => _closeMemo.runOnce(() async {
await _compilePool.close();
_frontendServerClient?.kill();
_frontendServerClient = null;
if (_outputDillDirectory.existsSync()) {
_outputDillDirectory.deleteSync(recursive: true);
}
});
}