| // 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. |
| 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.' |
| '${_dillCacheSuffix(_languageVersionComment, enabledExperiments)}'; |
| |
| 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, |
| enabledExperiments: enabledExperiments, |
| 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); |
| } |
| }); |
| } |
| |
| /// 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) { |
| identifierString.writeln(experiment); |
| } |
| return base64.encode(utf8.encode(identifierString.toString())); |
| } |