53219f95e6
This change introduces the dynamic_call test in pkg/dynamic_modules. TEST=pkg/dynamic_modules/test/data/dynamic_call Change-Id: I2425fd7925b2d7e66f919ceaaa102f4950e3a712 Bug: b/448095881 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/498301 Commit-Queue: Sigmund Cherem <sigmund@google.com> Reviewed-by: Alexander Markov <alexmarkov@google.com>
276 lines
8.6 KiB
Dart
276 lines
8.6 KiB
Dart
// 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 the Dart AOT runtime.
|
|
library;
|
|
|
|
import 'dart:io';
|
|
import 'package:front_end/src/util/trim.dart';
|
|
import 'package:kernel/target/targets.dart';
|
|
import 'package:vm/modular/target/vm.dart';
|
|
import '../common/testing.dart' as helper;
|
|
|
|
import 'model.dart';
|
|
import 'util.dart';
|
|
import 'target.dart';
|
|
|
|
enum VmMode { aot, jit }
|
|
|
|
/// Logic to build and execute dynamic modules using Dart VM (AOT or JIT).
|
|
///
|
|
/// In particular:
|
|
/// * The initial app is built as a regular AOT target, except that
|
|
/// a dynamic interface is used to retain any API that may be used by
|
|
/// dynamic modules.
|
|
/// * For dynamic modules, code is compiled to bytecode and we validate that
|
|
/// the module only accesses what's allowed by the dynamic interface.
|
|
/// * Unlike DDC, we don't use the embedder to load dynamic code, but include
|
|
/// the logic for that as part of the Dart application.
|
|
///
|
|
/// Note: this assumes that [Platform.resolvedExecutable] is a VM built with the
|
|
/// `--dart_dynamic_modules` build flag.
|
|
class VmExecutor implements TargetExecutor {
|
|
static const rootScheme = 'dev-dart-app';
|
|
final VmMode mode;
|
|
late final bool _shouldCleanup;
|
|
late final Directory _tmp;
|
|
final Logger _logger;
|
|
|
|
VmExecutor(this._logger, {required this.mode}) {
|
|
/// 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');
|
|
}
|
|
}
|
|
|
|
Future _buildKernel(
|
|
String testName,
|
|
String source,
|
|
Uri sourceDir,
|
|
bool isAot,
|
|
) async {
|
|
assert(source == 'main.dart');
|
|
var testDir = _tmp.uri.resolve(testName).toFilePath();
|
|
var aotTag = isAot ? "aot" : "no_aot";
|
|
var args = [
|
|
'--disable-dart-dev',
|
|
genKernelSnapshot.toFilePath(),
|
|
'--target',
|
|
'vm',
|
|
'--packages',
|
|
'${repoRoot.toFilePath()}/.dart_tool/package_config.json',
|
|
'-Ddart.vm.profile=false',
|
|
'-Ddart.vm.product=true',
|
|
'-Ddynamic.modules.test.mode=${mode.name}',
|
|
if (isAot) '--aot' else '--no-aot',
|
|
'--no-embed-sources',
|
|
'--platform',
|
|
vmPlatformDill.toFilePath(),
|
|
'--output',
|
|
'${source}_$aotTag.dill',
|
|
'--verbosity=all',
|
|
'--filesystem-root',
|
|
sourceDir.resolve('../../').toFilePath(),
|
|
'--filesystem-scheme',
|
|
rootScheme,
|
|
'--dynamic-interface',
|
|
'$rootScheme:/data/$testName/dynamic_interface.yaml',
|
|
'$rootScheme:/data/$testName/$source',
|
|
];
|
|
await runProcess(
|
|
aotRuntimeBin.toFilePath(),
|
|
args,
|
|
testDir,
|
|
_logger,
|
|
'kernel $aotTag $testName/$source',
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future compileApplication(DynamicModuleTest test) async {
|
|
_ensureDirectory(test.name);
|
|
_logger.info('Compile ${test.name} app');
|
|
|
|
if (mode == VmMode.aot) {
|
|
await _buildKernel(test.name, test.main, test.folder, true);
|
|
}
|
|
await _buildKernel(test.name, test.main, test.folder, false);
|
|
|
|
final testDir = _tmp.uri.resolve(test.name).toFilePath();
|
|
|
|
if (mode == VmMode.aot) {
|
|
var args = [
|
|
'--snapshot-kind=app-aot-elf',
|
|
'--elf=${test.main}.snapshot',
|
|
'${test.main}_aot.dill',
|
|
];
|
|
await runProcess(
|
|
genSnapshotBin.toFilePath(),
|
|
args,
|
|
testDir,
|
|
_logger,
|
|
'aot snapshot ${test.name}/${test.main}',
|
|
);
|
|
}
|
|
|
|
// The next steps are optional, but done to test trimming of assets used
|
|
// by the bytecode compiler.
|
|
|
|
await createTrimmedCopy(
|
|
TrimOptions(
|
|
inputAppPath: "$testDir/${test.main}_no_aot.dill",
|
|
outputAppPath: "$testDir/${test.main}_no_aot_trimmed.dill",
|
|
inputPlatformPath: vmPlatformDill.toFilePath(),
|
|
outputPlatformPath: "$testDir/${test.main}_platform_trimmed.dill",
|
|
dynamicInterfaceContents: File.fromUri(
|
|
test.folder.resolve('../../data/${test.name}/dynamic_interface.yaml'),
|
|
).readAsStringSync(),
|
|
dynamicInterfaceUri: Uri.parse(
|
|
'$rootScheme:/data/${test.name}/dynamic_interface.yaml',
|
|
),
|
|
requiredDartLibraries: VmTarget(
|
|
TargetFlags(),
|
|
).extraRequiredLibraries.toSet(),
|
|
),
|
|
);
|
|
|
|
void logSizeDiff(String path1, String path2) {
|
|
final originalSize = File(path1).statSync().size;
|
|
final trimmedSize = File(path2).statSync().size;
|
|
_logger.info(
|
|
'Size difference for $path2: '
|
|
'$originalSize => $trimmedSize '
|
|
'(${(trimmedSize * 100 / originalSize).toStringAsFixed(2)}%)',
|
|
);
|
|
}
|
|
|
|
logSizeDiff(
|
|
vmPlatformDill.toFilePath(),
|
|
'$testDir/${test.main}_platform_trimmed.dill',
|
|
);
|
|
logSizeDiff(
|
|
"$testDir/${test.main}_no_aot.dill",
|
|
"$testDir/${test.main}_no_aot_trimmed.dill",
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future compileDynamicModule(DynamicModuleTest test, String name) async {
|
|
_logger.info('Compile module ${test.name}.$name');
|
|
_ensureDirectory('${test.name}/modules');
|
|
var testDir = _tmp.uri.resolve(test.name).toFilePath();
|
|
var source = test.dynamicModules[name]!;
|
|
var args = [
|
|
'--disable-dart-dev',
|
|
dart2bytecodeSnapshot.toFilePath(),
|
|
'--platform',
|
|
'${test.main}_platform_trimmed.dill',
|
|
'--target',
|
|
'vm',
|
|
'--packages',
|
|
'${repoRoot.toFilePath()}/.dart_tool/package_config.json',
|
|
'-Ddart.vm.profile=false',
|
|
'-Ddart.vm.product=true',
|
|
'-Ddynamic.modules.test.mode=${mode.name}',
|
|
'--import-dill',
|
|
'${test.main}_no_aot_trimmed.dill',
|
|
'--validate',
|
|
'$rootScheme:/data/${test.name}/dynamic_interface.yaml',
|
|
// TODO(sigmund): provide a mechanism to specify options from each test.
|
|
'--bytecode-options=source-positions',
|
|
'--allow-dynamic-calls-in-dynamic-modules',
|
|
'--extra-selectors-allowed-in-dynamic-calls=m5,m6,m7,m10,m11,m15,m16,call',
|
|
'--verbosity=all',
|
|
'--filesystem-root',
|
|
test.folder.resolve('../../').toFilePath(),
|
|
'--filesystem-scheme',
|
|
rootScheme,
|
|
'--output',
|
|
'$source.bytecode',
|
|
if (test.name.endsWith('_prefixed'))
|
|
// unique prefix for each module
|
|
...[
|
|
'--prefix-library-uris',
|
|
name,
|
|
] else
|
|
// the same prefix for all modules
|
|
...[
|
|
'--prefix-library-uris',
|
|
'import/prefix',
|
|
],
|
|
'$rootScheme:/data/${test.name}/$source',
|
|
];
|
|
await runProcess(
|
|
aotRuntimeBin.toFilePath(),
|
|
args,
|
|
testDir,
|
|
_logger,
|
|
'compile bytecode ${test.name}/$source',
|
|
);
|
|
}
|
|
|
|
@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 result = await runProcess(
|
|
switch (mode) {
|
|
VmMode.aot => aotRuntimeBin.toFilePath(),
|
|
VmMode.jit => jitRuntimeBin.toFilePath(),
|
|
},
|
|
[
|
|
if (mode == VmMode.jit) '--check-dynamic-calls',
|
|
switch (mode) {
|
|
VmMode.aot => '${test.main}.snapshot',
|
|
VmMode.jit => '${test.main}_no_aot.dill',
|
|
},
|
|
],
|
|
testDir.toFilePath(),
|
|
_logger,
|
|
'executable test ${test.main}.exe',
|
|
);
|
|
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();
|
|
}
|
|
}
|
|
}
|