| // Copyright (c) 2024, 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. | |
| /// Implementation of the [TargetExecutor] for DDC. | |
| library; | |
| import 'dart:io'; | |
| import '../common/testing.dart' as helper; | |
| import 'model.dart'; | |
| import 'util.dart'; | |
| import 'target.dart'; | |
| /// Logic to build and execute dynamic modules in DDC. | |
| /// | |
| /// In particular: | |
| /// * The initial app is built as a regular DDC target, except that | |
| /// a dynamic interface is used to validate that the public API matches | |
| /// real declarations. | |
| /// * For dynamic modules, DDC validates that the module only accesses what's | |
| /// allowed by the dynamic interface. | |
| /// * For dynamic modules, DDC also produces a slighly different output | |
| /// to implement library isolation. | |
| /// * Tests are executed in d8 using a custom bootstrapping logic. Eventually | |
| /// this logic needs to be centralized inside the compiler. | |
| class DdcExecutor implements TargetExecutor { | |
| static const rootScheme = 'dev-dart-app'; | |
| late final bool _shouldCleanup; | |
| late final Directory _tmp; | |
| final Logger _logger; | |
| DdcExecutor(this._logger) { | |
| /// Allow using an environment variable to run tests on a fixed directory. | |
| /// This prevents the directory from getting deleted too. | |
| var path = Platform.environment['TMP_DIR'] ?? ''; | |
| if (path.isEmpty) { | |
| _tmp = Directory.systemTemp.createTempSync('_dynamic_module-'); | |
| _shouldCleanup = true; | |
| } else { | |
| _tmp = Directory(path); | |
| if (!_tmp.existsSync()) _tmp.createSync(); | |
| _shouldCleanup = false; | |
| } | |
| } | |
| @override | |
| Future<void> suiteComplete() async { | |
| if (!_shouldCleanup) return; | |
| try { | |
| _tmp.delete(recursive: true); | |
| } on FileSystemException { | |
| // Windows bots sometimes fail to delete folders, and can make tests | |
| // flaky. It is OK in those cases to leak some files in the tmp folder, | |
| // these will eventually be cleared when a new bot instance is created. | |
| _logger.warning('Error trying to delete $_tmp'); | |
| } | |
| } | |
| // TODO(sigmund): add support to run also in the ddc-canary mode. | |
| Future _compile( | |
| String testName, | |
| String source, | |
| Uri sourceDir, | |
| bool isMain, | |
| ) async { | |
| var testDir = _tmp.uri.resolve(testName).toFilePath(); | |
| var args = [ | |
| '--packages=${repoRoot.toFilePath()}/.dart_tool/package_config.json', | |
| ddcAotSnapshot.toFilePath(), | |
| '--modules=ddc', | |
| '--no-summarize', | |
| '--no-source-map', | |
| '--multi-root', | |
| '${sourceDir.resolve('../../')}', | |
| '--multi-root-scheme', | |
| rootScheme, | |
| '$rootScheme:/data/$testName/$source', | |
| '--dart-sdk-summary', | |
| '$ddcSdkOutline', | |
| // Note: this needs to change if we ever intend to support packages within | |
| // the dynamic loading tests themselves | |
| '--packages=$repoRoot/.dart_tool/package_config.json', | |
| if (!isMain) ...[ | |
| // TODO(sigmund): consider specifying the module name directly | |
| '--dynamic-module', | |
| '--summary=main.dart.dill=main.dart', | |
| ], | |
| '-o', | |
| '$source.js', | |
| ]; | |
| await runProcess( | |
| dartAotBin.toFilePath(), | |
| args, | |
| testDir, | |
| _logger, | |
| 'compile $testName/$source', | |
| ); | |
| } | |
| Future _buildKernelOutline( | |
| String testName, | |
| String source, | |
| Uri sourceDir, | |
| ) async { | |
| assert(source == 'main.dart'); | |
| var testDir = _tmp.uri.resolve(testName).toFilePath(); | |
| var args = [ | |
| '--packages=${repoRoot.toFilePath()}/.dart_tool/package_config.json', | |
| kernelWorkerAotSnapshot.toFilePath(), | |
| '--summary-only', | |
| '--target', | |
| 'ddc', | |
| '--multi-root', | |
| '${sourceDir.resolve('../../')}', | |
| '--multi-root-scheme', | |
| rootScheme, | |
| '--packages-file=$repoRoot/.dart_tool/package_config.json', | |
| '--dart-sdk-summary', | |
| '$ddcSdkOutline', | |
| '--source', | |
| '$rootScheme:/data/$testName/$source', | |
| '--output', | |
| '$source.dill', | |
| ]; | |
| await runProcess( | |
| dartAotBin.toFilePath(), | |
| args, | |
| testDir, | |
| _logger, | |
| 'sumarize $testName/$source', | |
| ); | |
| } | |
| @override | |
| Future compileApplication(DynamicModuleTest test) async { | |
| _ensureDirectory(test.name); | |
| _logger.info('Compile ${test.name} app'); | |
| await _buildKernelOutline(test.name, test.main, test.folder); | |
| await _compile(test.name, test.main, test.folder, true); | |
| } | |
| @override | |
| Future compileDynamicModule(DynamicModuleTest test, String name) async { | |
| _logger.info('Compile module ${test.name}.$name'); | |
| _ensureDirectory(test.name); | |
| await _compile(test.name, test.dynamicModules[name]!, test.folder, false); | |
| } | |
| @override | |
| Future executeApplication(DynamicModuleTest test) async { | |
| _logger.info('Execute ${test.name}'); | |
| _ensureDirectory(test.name); | |
| // We generate a self contained script that loads necessary preambles, | |
| // ddc module loader, the necessary modules (the SDK and the main module), | |
| // and finally launches the app. | |
| var testDir = _tmp.uri.resolve('${test.name}/'); | |
| var bootstrapUri = testDir.resolve('bootstrap.js'); | |
| // TODO(sigmund): remove hardwired entrypoint name | |
| String toPath(Uri uri) => uri.toFilePath().replaceAll('\\', '\\\\'); | |
| File.fromUri(bootstrapUri).writeAsStringSync(''' | |
| load('${toPath(ddcPreamblesJs)}'); // preambles/d8.js | |
| load('${toPath(ddcSealNativeObjectJs)}'); // seal_native_object.js | |
| load('${toPath(ddcModuleLoaderJs)}'); // ddc_module_loader.js | |
| load('${toPath(ddcSdkJs)}'); // dart_sdk.js | |
| load('main.dart.js'); // compiled test module | |
| self.dartMainRunner(function () { | |
| dart_library.configure( | |
| "_dynamic_module_test", // app name | |
| { | |
| dynamicModuleLoader: (uri, onLoad) => { | |
| let name = uri; // our test framework simply | |
| // provides the module name as | |
| // the uri. | |
| load(`modules/\${name}.js`); | |
| onLoad(name); | |
| }, | |
| }); | |
| dart_library.start( | |
| "_dynamic_module_test", // app name | |
| '00000000-0000-0000-0000-000000000000', // uuid | |
| "main.dart", // module | |
| "data__${test.name}__main", // library containing main | |
| false | |
| ); | |
| }); | |
| '''); | |
| var result = await runProcess( | |
| d8Uri.toFilePath(), | |
| [bootstrapUri.toFilePath()], | |
| testDir.toFilePath(), | |
| _logger, | |
| 'd8 ${test.name}/bootstrap.js', | |
| ); | |
| var stdout = result.stdout as String; | |
| if (!stdout.contains(helper.successToken)) { | |
| _logger.error( | |
| 'Error: test didn\'t complete as expected.\n' | |
| 'Make sure the test finishes and calls `helper.done()`.\n' | |
| 'Test output:\n$stdout', | |
| ); | |
| throw Exception('missing helper.done'); | |
| } | |
| } | |
| void _ensureDirectory(String name) { | |
| var dir = Directory.fromUri(_tmp.uri.resolve(name)); | |
| if (!dir.existsSync()) { | |
| dir.createSync(); | |
| } | |
| } | |
| } |