| // 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(); |
| } |
| } |
| } |