blob: ef77ba5d9f40981822b73170835d51152ae0bca3 [file] [log] [blame]
// 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();
}
}
}